@workflow-manager/runner 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +123 -12
  2. package/dist/adapters.d.ts +4 -0
  3. package/dist/adapters.js +11 -0
  4. package/dist/claudeCodeExecutor.d.ts +4 -0
  5. package/dist/claudeCodeExecutor.js +173 -0
  6. package/dist/cliRunRenderer.d.ts +36 -0
  7. package/dist/cliRunRenderer.js +286 -0
  8. package/dist/engine.d.ts +4 -2
  9. package/dist/engine.js +622 -44
  10. package/dist/events.d.ts +1 -1
  11. package/dist/events.js +4 -2
  12. package/dist/index.js +371 -41
  13. package/dist/manPage.d.ts +1 -0
  14. package/dist/manPage.js +147 -0
  15. package/dist/mockExecutor.d.ts +2 -2
  16. package/dist/mockExecutor.js +62 -13
  17. package/dist/opencodeExecutor.d.ts +2 -2
  18. package/dist/opencodeExecutor.js +101 -138
  19. package/dist/parser.js +98 -2
  20. package/dist/piAgentExecutor.d.ts +4 -0
  21. package/dist/piAgentExecutor.js +298 -0
  22. package/dist/remote/api.d.ts +1 -0
  23. package/dist/remote/commands.d.ts +2 -0
  24. package/dist/remote/commands.js +76 -4
  25. package/dist/runnerApi.d.ts +7 -0
  26. package/dist/runnerApi.js +221 -0
  27. package/dist/runnerSession.d.ts +62 -0
  28. package/dist/runnerSession.js +260 -0
  29. package/dist/runtimePreflight.d.ts +16 -0
  30. package/dist/runtimePreflight.js +189 -0
  31. package/dist/skillResolver.d.ts +6 -0
  32. package/dist/skillResolver.js +75 -0
  33. package/dist/types.d.ts +148 -2
  34. package/man/wfm.1 +54 -4
  35. package/package.json +28 -4
  36. package/skills/commit-discipline/SKILL.md +109 -0
  37. package/skills/doc-sync/SKILL.md +83 -0
  38. package/skills/repo-hygiene/SKILL.md +70 -0
  39. package/skills/spec-driven-development/SKILL.md +33 -0
  40. package/skills/workflow-manager-cli/SKILL.md +14 -9
@@ -0,0 +1,221 @@
1
+ import http from "node:http";
2
+ function jsonResponse(res, status, body) {
3
+ const payload = JSON.stringify(body);
4
+ res.statusCode = status;
5
+ res.setHeader("Content-Type", "application/json");
6
+ res.setHeader("Content-Length", Buffer.byteLength(payload));
7
+ res.end(payload);
8
+ }
9
+ function errorResponse(res, status, error, message) {
10
+ jsonResponse(res, status, { error, message });
11
+ }
12
+ function isAuthorized(req, store) {
13
+ const header = req.headers.authorization;
14
+ return header === `Bearer ${store.attachToken()}`;
15
+ }
16
+ function parseBoolean(value, fallback) {
17
+ if (value === null) {
18
+ return fallback;
19
+ }
20
+ return value !== "false";
21
+ }
22
+ async function readJsonBody(req) {
23
+ const chunks = [];
24
+ for await (const chunk of req) {
25
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
26
+ }
27
+ if (chunks.length === 0) {
28
+ return {};
29
+ }
30
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
31
+ if (!raw) {
32
+ return {};
33
+ }
34
+ const parsed = JSON.parse(raw);
35
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
36
+ throw new Error("Request body must be a JSON object");
37
+ }
38
+ return parsed;
39
+ }
40
+ export async function startRunnerApiServer(store, requestedPort) {
41
+ const sockets = new Set();
42
+ const server = http.createServer((req, res) => {
43
+ void (async () => {
44
+ const method = req.method ?? "GET";
45
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
46
+ const pathname = url.pathname;
47
+ if (method === "GET" && pathname === "/health") {
48
+ jsonResponse(res, 200, { ok: true });
49
+ return;
50
+ }
51
+ if (!isAuthorized(req, store)) {
52
+ errorResponse(res, 401, "unauthorized", "Missing or invalid attach token");
53
+ return;
54
+ }
55
+ if (method === "GET" && pathname === "/session") {
56
+ jsonResponse(res, 200, store.publicSession());
57
+ return;
58
+ }
59
+ const runMatch = pathname.match(/^\/runs\/([^/]+)$/);
60
+ if (method === "GET" && runMatch) {
61
+ const runId = decodeURIComponent(runMatch[1] ?? "");
62
+ if (!store.isKnownRun(runId)) {
63
+ errorResponse(res, 404, "not_found", `Unknown run: ${runId}`);
64
+ return;
65
+ }
66
+ jsonResponse(res, 200, store.snapshot());
67
+ return;
68
+ }
69
+ const stepMatch = pathname.match(/^\/runs\/([^/]+)\/steps\/([^/]+)$/);
70
+ if (method === "GET" && stepMatch) {
71
+ const runId = decodeURIComponent(stepMatch[1] ?? "");
72
+ const stepKey = decodeURIComponent(stepMatch[2] ?? "");
73
+ if (!store.isKnownRun(runId)) {
74
+ errorResponse(res, 404, "not_found", `Unknown run: ${runId}`);
75
+ return;
76
+ }
77
+ const step = store.stepDetail(stepKey);
78
+ if (!step) {
79
+ errorResponse(res, 404, "not_found", `Unknown step: ${stepKey}`);
80
+ return;
81
+ }
82
+ jsonResponse(res, 200, step);
83
+ return;
84
+ }
85
+ const logsMatch = pathname.match(/^\/runs\/([^/]+)\/logs$/);
86
+ if (method === "GET" && logsMatch) {
87
+ const runId = decodeURIComponent(logsMatch[1] ?? "");
88
+ if (!store.isKnownRun(runId)) {
89
+ errorResponse(res, 404, "not_found", `Unknown run: ${runId}`);
90
+ return;
91
+ }
92
+ const stepKey = url.searchParams.get("stepKey") ?? undefined;
93
+ const limit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
94
+ const cursor = url.searchParams.get("cursor") ?? undefined;
95
+ jsonResponse(res, 200, store.listLogs(stepKey, limit, cursor));
96
+ return;
97
+ }
98
+ const controlMatch = pathname.match(/^\/runs\/([^/]+)\/(approve|resume|cancel)$/);
99
+ if (method === "POST" && controlMatch) {
100
+ const runId = decodeURIComponent(controlMatch[1] ?? "");
101
+ const action = controlMatch[2] ?? "";
102
+ if (!store.isKnownRun(runId)) {
103
+ errorResponse(res, 404, "not_found", `Unknown run: ${runId}`);
104
+ return;
105
+ }
106
+ let body;
107
+ try {
108
+ body = await readJsonBody(req);
109
+ }
110
+ catch (error) {
111
+ errorResponse(res, 400, "invalid_body", error.message);
112
+ return;
113
+ }
114
+ const stepKey = typeof body.stepKey === "string" ? body.stepKey : undefined;
115
+ const metadata = {
116
+ actor: typeof body.actor === "string" ? body.actor : undefined,
117
+ note: typeof body.note === "string" ? body.note : undefined,
118
+ source: typeof body.source === "string" ? body.source : "api",
119
+ };
120
+ const result = action === "cancel"
121
+ ? store.cancel(stepKey, metadata)
122
+ : action === "resume"
123
+ ? store.resume(stepKey, metadata)
124
+ : store.approve(stepKey, metadata);
125
+ if (!result.ok) {
126
+ errorResponse(res, 409, "conflict", result.reason ?? "Unable to update run state");
127
+ return;
128
+ }
129
+ jsonResponse(res, 200, {
130
+ ok: true,
131
+ decision: action === "cancel" ? "cancelled" : "approved",
132
+ stepKey: result.stepKey ?? stepKey ?? null,
133
+ actor: metadata.actor ?? null,
134
+ note: metadata.note ?? null,
135
+ source: metadata.source,
136
+ });
137
+ return;
138
+ }
139
+ const eventsMatch = pathname.match(/^\/runs\/([^/]+)\/events$/);
140
+ if (method === "GET" && eventsMatch) {
141
+ const runId = decodeURIComponent(eventsMatch[1] ?? "");
142
+ if (!store.isKnownRun(runId)) {
143
+ errorResponse(res, 404, "not_found", `Unknown run: ${runId}`);
144
+ return;
145
+ }
146
+ const sinceSequence = Number.parseInt(url.searchParams.get("sinceSequence") ?? "0", 10) || undefined;
147
+ const includeLogs = parseBoolean(url.searchParams.get("includeLogs"), true);
148
+ res.statusCode = 200;
149
+ res.setHeader("Content-Type", "text/event-stream");
150
+ res.setHeader("Cache-Control", "no-cache");
151
+ res.setHeader("Connection", "keep-alive");
152
+ res.flushHeaders?.();
153
+ for (const event of store.events(sinceSequence, includeLogs)) {
154
+ res.write(`event: ${event.type}\n`);
155
+ res.write(`id: ${event.sequence}\n`);
156
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
157
+ }
158
+ const heartbeat = setInterval(() => {
159
+ res.write("event: heartbeat\n");
160
+ res.write(`data: ${JSON.stringify({ occurredAt: new Date().toISOString() })}\n\n`);
161
+ }, 15000);
162
+ const unsubscribe = store.subscribe((event) => {
163
+ if (!includeLogs && (event.type === "agent.stdout" || event.type === "agent.stderr")) {
164
+ return;
165
+ }
166
+ res.write(`event: ${event.type}\n`);
167
+ res.write(`id: ${event.sequence}\n`);
168
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
169
+ });
170
+ const cleanup = () => {
171
+ clearInterval(heartbeat);
172
+ unsubscribe();
173
+ };
174
+ req.on("close", cleanup);
175
+ res.on("close", cleanup);
176
+ return;
177
+ }
178
+ errorResponse(res, 404, "not_found", `Unknown endpoint: ${pathname}`);
179
+ })().catch((error) => {
180
+ if (!res.headersSent) {
181
+ errorResponse(res, 500, "internal_error", error.message);
182
+ return;
183
+ }
184
+ res.destroy(error);
185
+ });
186
+ });
187
+ server.on("connection", (socket) => {
188
+ sockets.add(socket);
189
+ socket.on("close", () => {
190
+ sockets.delete(socket);
191
+ });
192
+ });
193
+ await new Promise((resolve, reject) => {
194
+ server.once("error", reject);
195
+ server.listen(requestedPort, "127.0.0.1", () => {
196
+ server.off("error", reject);
197
+ resolve();
198
+ });
199
+ });
200
+ const address = server.address();
201
+ if (!address || typeof address === "string") {
202
+ throw new Error("Runner API failed to bind to an address");
203
+ }
204
+ const { port } = address;
205
+ store.setBinding("127.0.0.1", port);
206
+ return {
207
+ port,
208
+ close: () => new Promise((resolve, reject) => {
209
+ for (const socket of sockets) {
210
+ socket.destroy();
211
+ }
212
+ server.close((error) => {
213
+ if (error) {
214
+ reject(error);
215
+ return;
216
+ }
217
+ resolve();
218
+ });
219
+ }),
220
+ };
221
+ }
@@ -0,0 +1,62 @@
1
+ import type { ApprovalDecisionPayload, ApprovalRequest, RunEvent, RunController, RunObserver, RunnerEventEnvelope, RunnerLogChunk, RunnerSessionInfo, RunSnapshot, StepDetailSnapshot, WorkflowDefinition } from "./types.js";
2
+ interface RunnerSessionStoreOptions {
3
+ runId: string;
4
+ workflow: WorkflowDefinition;
5
+ objective: string;
6
+ objectives: string[];
7
+ host?: string;
8
+ port?: number;
9
+ attachToken?: string;
10
+ startedAt?: string;
11
+ sessionId?: string;
12
+ }
13
+ interface LogListResult {
14
+ items: RunnerLogChunk[];
15
+ nextCursor: string | null;
16
+ }
17
+ type Subscriber = (event: RunnerEventEnvelope) => void;
18
+ export declare class RunnerSessionStore implements RunObserver, RunController {
19
+ private readonly workflow;
20
+ private readonly subscribers;
21
+ private readonly maxLogChunks;
22
+ private snapshotState;
23
+ private stepDetailsState;
24
+ private readonly eventHistory;
25
+ private readonly logHistory;
26
+ private readonly sessionState;
27
+ private pendingApproval;
28
+ constructor(options: RunnerSessionStoreOptions);
29
+ setBinding(host: string, port: number): void;
30
+ sessionInfo(): RunnerSessionInfo;
31
+ attachToken(): string;
32
+ runId(): string;
33
+ snapshot(): RunSnapshot;
34
+ stepDetail(stepKey: string): StepDetailSnapshot | null;
35
+ listLogs(stepKey?: string, limit?: number, cursor?: string): LogListResult;
36
+ events(sinceSequence?: number, includeLogs?: boolean): RunnerEventEnvelope[];
37
+ subscribe(listener: Subscriber): () => void;
38
+ onEvent(event: RunEvent): void;
39
+ onSnapshot(snapshot: RunSnapshot, stepDetails: StepDetailSnapshot[]): void;
40
+ onLog(log: RunnerLogChunk): void;
41
+ publicSession(): Omit<RunnerSessionInfo, "attachToken">;
42
+ isKnownRun(runId: string): boolean;
43
+ workflowDefinition(): WorkflowDefinition;
44
+ waitForDecision(request: ApprovalRequest): Promise<ApprovalDecisionPayload>;
45
+ approve(stepKey?: string, metadata?: Omit<ApprovalDecisionPayload, "decision">): {
46
+ ok: boolean;
47
+ reason?: string;
48
+ stepKey?: string;
49
+ };
50
+ resume(stepKey?: string, metadata?: Omit<ApprovalDecisionPayload, "decision">): {
51
+ ok: boolean;
52
+ reason?: string;
53
+ stepKey?: string;
54
+ };
55
+ cancel(stepKey?: string, metadata?: Omit<ApprovalDecisionPayload, "decision">): {
56
+ ok: boolean;
57
+ reason?: string;
58
+ stepKey?: string;
59
+ };
60
+ private resolvePendingApproval;
61
+ }
62
+ export {};
@@ -0,0 +1,260 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveTaskAdapter } from "./adapters.js";
3
+ function contextSummary(value) {
4
+ if (typeof value === "string") {
5
+ return { type: "string", length: value.length };
6
+ }
7
+ if (value && typeof value === "object") {
8
+ return { type: "object", keys: Object.keys(value).sort() };
9
+ }
10
+ return { type: "none" };
11
+ }
12
+ function stepConfig(step) {
13
+ return {
14
+ model: typeof step.taskSpec?.init?.model === "string" ? step.taskSpec.init.model : null,
15
+ skills: step.taskSpec?.init?.skills ?? [],
16
+ mcps: step.taskSpec?.init?.mcps ?? [],
17
+ systemPrompts: step.taskSpec?.init?.systemPrompts ?? [],
18
+ contextSummary: contextSummary(step.taskSpec?.init?.context),
19
+ };
20
+ }
21
+ function cloneSnapshot(snapshot) {
22
+ return {
23
+ ...snapshot,
24
+ objectives: [...snapshot.objectives],
25
+ waitingForApproval: snapshot.waitingForApproval
26
+ ? {
27
+ ...snapshot.waitingForApproval,
28
+ preview: snapshot.waitingForApproval.preview
29
+ ? {
30
+ ...snapshot.waitingForApproval.preview,
31
+ items: snapshot.waitingForApproval.preview.items.map((item) => ({ ...item })),
32
+ }
33
+ : null,
34
+ }
35
+ : null,
36
+ steps: snapshot.steps.map((step) => ({ ...step })),
37
+ };
38
+ }
39
+ function cloneStepDetails(stepDetails) {
40
+ return stepDetails.map((detail) => ({
41
+ ...detail,
42
+ dependsOn: [...detail.dependsOn],
43
+ config: {
44
+ ...detail.config,
45
+ skills: [...detail.config.skills],
46
+ mcps: [...detail.config.mcps],
47
+ systemPrompts: [...detail.config.systemPrompts],
48
+ contextSummary: {
49
+ ...detail.config.contextSummary,
50
+ keys: detail.config.contextSummary.keys ? [...detail.config.contextSummary.keys] : undefined,
51
+ },
52
+ },
53
+ lastExecution: { ...detail.lastExecution },
54
+ }));
55
+ }
56
+ function envelopeFromEvent(event) {
57
+ return {
58
+ id: event.id,
59
+ sequence: event.sequenceNumber,
60
+ type: event.type,
61
+ runId: event.runId,
62
+ stepKey: event.stepRunId,
63
+ occurredAt: event.occurredAt,
64
+ data: { ...event.payload },
65
+ };
66
+ }
67
+ export class RunnerSessionStore {
68
+ workflow;
69
+ subscribers = new Set();
70
+ maxLogChunks = 2000;
71
+ snapshotState;
72
+ stepDetailsState;
73
+ eventHistory = [];
74
+ logHistory = [];
75
+ sessionState;
76
+ pendingApproval = null;
77
+ constructor(options) {
78
+ this.workflow = options.workflow;
79
+ const startedAt = options.startedAt ?? new Date().toISOString();
80
+ const host = options.host ?? "127.0.0.1";
81
+ const port = options.port ?? 0;
82
+ const attachToken = options.attachToken ?? randomUUID();
83
+ const steps = options.workflow.steps.map((step) => ({
84
+ stepKey: step.key,
85
+ status: "pending",
86
+ attempt: 0,
87
+ confirmed: false,
88
+ adapter: step.kind === "task" ? resolveTaskAdapter(step.taskSpec?.adapterKey) : "approval",
89
+ startedAt: null,
90
+ updatedAt: startedAt,
91
+ finishedAt: null,
92
+ }));
93
+ const emptyExecution = {
94
+ executionStatus: null,
95
+ qaAction: null,
96
+ feedbackReason: null,
97
+ };
98
+ this.stepDetailsState = options.workflow.steps.map((step, index) => ({
99
+ ...steps[index],
100
+ kind: step.kind,
101
+ objective: step.objective ?? step.title ?? null,
102
+ dependsOn: step.dependsOn ?? [],
103
+ config: stepConfig(step),
104
+ lastExecution: { ...emptyExecution },
105
+ }));
106
+ this.snapshotState = {
107
+ runId: options.runId,
108
+ workflowKey: options.workflow.key,
109
+ workflowTitle: options.workflow.title,
110
+ status: "queued",
111
+ currentStepKey: null,
112
+ startedAt: null,
113
+ updatedAt: startedAt,
114
+ endedAt: null,
115
+ objective: options.objective,
116
+ objectives: [...options.objectives],
117
+ waitingForApproval: null,
118
+ steps,
119
+ };
120
+ this.sessionState = {
121
+ sessionId: options.sessionId ?? randomUUID(),
122
+ pid: process.pid,
123
+ host,
124
+ port,
125
+ baseUrl: `http://${host}:${port}`,
126
+ attachToken,
127
+ startedAt,
128
+ run: {
129
+ runId: options.runId,
130
+ workflowKey: options.workflow.key,
131
+ workflowTitle: options.workflow.title,
132
+ status: "queued",
133
+ },
134
+ };
135
+ }
136
+ setBinding(host, port) {
137
+ this.sessionState.host = host;
138
+ this.sessionState.port = port;
139
+ this.sessionState.baseUrl = `http://${host}:${port}`;
140
+ }
141
+ sessionInfo() {
142
+ return {
143
+ ...this.sessionState,
144
+ run: { ...this.sessionState.run },
145
+ };
146
+ }
147
+ attachToken() {
148
+ return this.sessionState.attachToken;
149
+ }
150
+ runId() {
151
+ return this.snapshotState.runId;
152
+ }
153
+ snapshot() {
154
+ return cloneSnapshot(this.snapshotState);
155
+ }
156
+ stepDetail(stepKey) {
157
+ const detail = this.stepDetailsState.find((step) => step.stepKey === stepKey);
158
+ if (!detail) {
159
+ return null;
160
+ }
161
+ return cloneStepDetails([detail])[0] ?? null;
162
+ }
163
+ listLogs(stepKey, limit = 200, cursor) {
164
+ const parsedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 200;
165
+ const filtered = stepKey ? this.logHistory.filter((log) => log.stepKey === stepKey) : this.logHistory;
166
+ const offset = cursor ? Math.max(0, Number.parseInt(cursor, 10) || 0) : 0;
167
+ const items = filtered.slice(offset, offset + parsedLimit).map((item) => ({ ...item }));
168
+ const nextCursor = offset + parsedLimit < filtered.length ? String(offset + parsedLimit) : null;
169
+ return { items, nextCursor };
170
+ }
171
+ events(sinceSequence, includeLogs = true) {
172
+ return this.eventHistory
173
+ .filter((event) => (sinceSequence ? event.sequence > sinceSequence : true))
174
+ .filter((event) => includeLogs || (event.type !== "agent.stdout" && event.type !== "agent.stderr"))
175
+ .map((event) => ({ ...event, data: { ...event.data } }));
176
+ }
177
+ subscribe(listener) {
178
+ this.subscribers.add(listener);
179
+ return () => {
180
+ this.subscribers.delete(listener);
181
+ };
182
+ }
183
+ onEvent(event) {
184
+ const envelope = envelopeFromEvent(event);
185
+ this.eventHistory.push(envelope);
186
+ for (const subscriber of this.subscribers) {
187
+ subscriber(envelope);
188
+ }
189
+ }
190
+ onSnapshot(snapshot, stepDetails) {
191
+ this.snapshotState = cloneSnapshot(snapshot);
192
+ this.stepDetailsState = cloneStepDetails(stepDetails);
193
+ this.sessionState.run.status = snapshot.status;
194
+ }
195
+ onLog(log) {
196
+ this.logHistory.push({ ...log });
197
+ if (this.logHistory.length > this.maxLogChunks) {
198
+ this.logHistory.splice(0, this.logHistory.length - this.maxLogChunks);
199
+ }
200
+ }
201
+ publicSession() {
202
+ const { attachToken: _attachToken, ...publicInfo } = this.sessionInfo();
203
+ return publicInfo;
204
+ }
205
+ isKnownRun(runId) {
206
+ return this.snapshotState.runId === runId;
207
+ }
208
+ workflowDefinition() {
209
+ return this.workflow;
210
+ }
211
+ waitForDecision(request) {
212
+ if (this.pendingApproval?.stepKey === request.stepKey) {
213
+ return this.pendingApproval.promise;
214
+ }
215
+ let resolveDecision;
216
+ const promise = new Promise((resolve) => {
217
+ resolveDecision = resolve;
218
+ });
219
+ this.pendingApproval = {
220
+ ...request,
221
+ promise,
222
+ resolve: resolveDecision,
223
+ };
224
+ return promise;
225
+ }
226
+ approve(stepKey, metadata = {}) {
227
+ return this.resolvePendingApproval("approved", stepKey, metadata, "human");
228
+ }
229
+ resume(stepKey, metadata = {}) {
230
+ return this.resolvePendingApproval("approved", stepKey, metadata, "external");
231
+ }
232
+ cancel(stepKey, metadata = {}) {
233
+ return this.resolvePendingApproval("cancelled", stepKey, metadata);
234
+ }
235
+ resolvePendingApproval(decision, stepKey, metadata = {}, expectedValidation) {
236
+ if (!this.pendingApproval) {
237
+ return { ok: false, reason: "No step is waiting for approval" };
238
+ }
239
+ if (stepKey && this.pendingApproval.stepKey !== stepKey) {
240
+ return {
241
+ ok: false,
242
+ reason: `Step ${stepKey} is not currently waiting for approval`,
243
+ };
244
+ }
245
+ // Waits with validation "none" (or unset) declare no expected resolution verb,
246
+ // so both approve and resume must be able to release them.
247
+ const pendingValidation = this.pendingApproval.validation ?? "none";
248
+ if (expectedValidation && pendingValidation !== "none" && pendingValidation !== expectedValidation) {
249
+ const action = expectedValidation === "external" ? "resume" : "approve";
250
+ return {
251
+ ok: false,
252
+ reason: `Cannot ${action} ${pendingValidation} validation for ${this.pendingApproval.stepKey}`,
253
+ };
254
+ }
255
+ const pending = this.pendingApproval;
256
+ this.pendingApproval = null;
257
+ pending.resolve({ decision, ...metadata });
258
+ return { ok: true, stepKey: pending.stepKey };
259
+ }
260
+ }
@@ -0,0 +1,16 @@
1
+ import type { AdapterKey, WorkflowDefinition } from "./types.js";
2
+ export interface RuntimeDoctorCheck {
3
+ key: string;
4
+ label: string;
5
+ status: "ok" | "missing" | "info";
6
+ required: boolean;
7
+ detail: string;
8
+ }
9
+ export interface AdapterImplementationStatus {
10
+ adapter: AdapterKey;
11
+ status: "real" | "mock" | "partial";
12
+ detail: string;
13
+ }
14
+ export declare function validateRuntimeRequirements(definition: WorkflowDefinition, env?: NodeJS.ProcessEnv): string[];
15
+ export declare function runtimeDoctorChecks(env?: NodeJS.ProcessEnv): RuntimeDoctorCheck[];
16
+ export declare function adapterImplementationStatuses(): AdapterImplementationStatus[];