@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
package/dist/manPage.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export const MAN_PAGE_SOURCE = `.TH WFM 1 "April 2026" "@workflow-manager/runner" "User Commands"
|
|
2
|
+
.SH NAME
|
|
3
|
+
wfm \\- run markdown or json workflows from the CLI
|
|
4
|
+
.SH SYNOPSIS
|
|
5
|
+
.B wfm
|
|
6
|
+
.I command
|
|
7
|
+
[options]
|
|
8
|
+
.SH DESCRIPTION
|
|
9
|
+
wfm parses a workflow definition file, validates it, and executes
|
|
10
|
+
it with deterministic in-memory orchestration.
|
|
11
|
+
|
|
12
|
+
Workflow files can be Markdown with YAML frontmatter or JSON.
|
|
13
|
+
.SH COMMANDS
|
|
14
|
+
.TP
|
|
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.
|
|
20
|
+
.TP
|
|
21
|
+
.B scaffold [path] [--format markdown|json]
|
|
22
|
+
Create a starter workflow file. Format defaults to markdown unless the output
|
|
23
|
+
path ends in .json.
|
|
24
|
+
.TP
|
|
25
|
+
.B validate <workflow.md|workflow.json>
|
|
26
|
+
Validate workflow structure and report schema errors.
|
|
27
|
+
.TP
|
|
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.
|
|
39
|
+
.TP
|
|
40
|
+
.B auth <login|whoami|logout> [--token value]
|
|
41
|
+
Manage remote registry authentication for CLI publish and pull flows.
|
|
42
|
+
.TP
|
|
43
|
+
.B publish <workflow.md|workflow.json> [--slug slug] [--title text] [--description text] [--visibility public|private] [--version label] [--tag a,b] [--draft]
|
|
44
|
+
Publish a validated local workflow to the remote registry.
|
|
45
|
+
.TP
|
|
46
|
+
.B pull <owner/slug> [--version label] [--output path]
|
|
47
|
+
Download a remote workflow and write it to a local file.
|
|
48
|
+
.TP
|
|
49
|
+
.B search [query]
|
|
50
|
+
Search public workflows from the remote registry.
|
|
51
|
+
.TP
|
|
52
|
+
.B remote info <owner/slug>
|
|
53
|
+
Show metadata and source information for a remote workflow.
|
|
54
|
+
.TP
|
|
55
|
+
.B man
|
|
56
|
+
Open this man page.
|
|
57
|
+
.SH RUN OPTIONS
|
|
58
|
+
.TP
|
|
59
|
+
.B --input <path>
|
|
60
|
+
JSON file merged into global workflow input state.
|
|
61
|
+
.TP
|
|
62
|
+
.B --objective <text>
|
|
63
|
+
Override the default run objective.
|
|
64
|
+
.TP
|
|
65
|
+
.B --confirm <stepA,stepB:human,...>
|
|
66
|
+
Provide explicit confirmations for steps that require validation.
|
|
67
|
+
.TP
|
|
68
|
+
.B --auto-confirm-all
|
|
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.
|
|
99
|
+
.SH EXAMPLES
|
|
100
|
+
.TP
|
|
101
|
+
Validate markdown workflow:
|
|
102
|
+
.B wfm validate ./example-workflow.md
|
|
103
|
+
.TP
|
|
104
|
+
Validate json workflow:
|
|
105
|
+
.B wfm validate ./example-workflow.json
|
|
106
|
+
.TP
|
|
107
|
+
Scaffold json workflow file:
|
|
108
|
+
.B wfm scaffold ./new-workflow.json --format json
|
|
109
|
+
.TP
|
|
110
|
+
Authenticate with a CLI token:
|
|
111
|
+
.B wfm auth login --token wm_exampletoken
|
|
112
|
+
.TP
|
|
113
|
+
Publish a workflow:
|
|
114
|
+
.B wfm publish ./example-workflow.json --visibility public --tag example,automation
|
|
115
|
+
.TP
|
|
116
|
+
Pull a remote workflow:
|
|
117
|
+
.B wfm pull alice/remote-bunny --output ./remote-bunny.json
|
|
118
|
+
.TP
|
|
119
|
+
Search the remote registry:
|
|
120
|
+
.B wfm search bunny
|
|
121
|
+
.TP
|
|
122
|
+
Run with explicit confirmations:
|
|
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
|
|
133
|
+
.SH FILES
|
|
134
|
+
.TP
|
|
135
|
+
.B man/wfm.1
|
|
136
|
+
The manual page source shipped with this repository.
|
|
137
|
+
.SH EXIT STATUS
|
|
138
|
+
.TP
|
|
139
|
+
.B 0
|
|
140
|
+
Successful command execution.
|
|
141
|
+
.TP
|
|
142
|
+
.B 1
|
|
143
|
+
Validation or runtime error.
|
|
144
|
+
.TP
|
|
145
|
+
.B 2
|
|
146
|
+
Run completed in non-success terminal status.
|
|
147
|
+
`;
|
package/dist/mockExecutor.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { InputEnvelope, OutputEnvelope, StepDefinition } from "./types.js";
|
|
2
|
-
export declare function executeMockStep(step: StepDefinition, input: InputEnvelope, attempt: number): OutputEnvelope
|
|
1
|
+
import type { InputEnvelope, OutputEnvelope, StepDefinition, StepExecutionHooks } from "./types.js";
|
|
2
|
+
export declare function executeMockStep(step: StepDefinition, input: InputEnvelope, attempt: number, hooks?: StepExecutionHooks): Promise<OutputEnvelope>;
|
package/dist/mockExecutor.js
CHANGED
|
@@ -6,11 +6,39 @@ function chapterOutputs(previousOutput) {
|
|
|
6
6
|
.map((entry) => asRecord(entry))
|
|
7
7
|
.filter((entry) => typeof entry.chapterMarkdown === "string");
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
function asStringList(value) {
|
|
10
|
+
if (!Array.isArray(value)) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return value.map((entry) => String(entry));
|
|
14
|
+
}
|
|
15
|
+
async function emitStreamChunks(hooks, stream, chunks, delayMs) {
|
|
16
|
+
if (chunks.length === 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const emit = stream === "stdout" ? hooks?.onStdout : hooks?.onStderr;
|
|
20
|
+
for (const chunk of chunks) {
|
|
21
|
+
emit?.(chunk);
|
|
22
|
+
if (delayMs > 0) {
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function executeMockStep(step, input, attempt, hooks) {
|
|
10
28
|
const start = Date.now();
|
|
11
29
|
const payload = asRecord(step.taskSpec?.payload);
|
|
30
|
+
const delayMs = Number(payload.delayMs ?? 0);
|
|
31
|
+
const chunkDelayMs = Number(payload.chunkDelayMs ?? 0);
|
|
12
32
|
const mockResult = String(payload.mockResult ?? "success").toLowerCase();
|
|
13
33
|
const feedback = String(payload.feedback ?? "");
|
|
34
|
+
const stdoutChunks = asStringList(payload.stdoutChunks);
|
|
35
|
+
const stderrChunks = asStringList(payload.stderrChunks);
|
|
36
|
+
hooks?.onStarted?.({ adapter: input.priming_configuration.adapter ?? "mock" });
|
|
37
|
+
await emitStreamChunks(hooks, "stdout", stdoutChunks, Number.isFinite(chunkDelayMs) ? Math.max(0, Math.floor(chunkDelayMs)) : 0);
|
|
38
|
+
await emitStreamChunks(hooks, "stderr", stderrChunks, Number.isFinite(chunkDelayMs) ? Math.max(0, Math.floor(chunkDelayMs)) : 0);
|
|
39
|
+
if (Number.isFinite(delayMs) && delayMs > 0) {
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, Math.floor(delayMs)));
|
|
41
|
+
}
|
|
14
42
|
const make = (status, action, extraPayload = {}, feedbackReason = feedback) => ({
|
|
15
43
|
step_id: step.key,
|
|
16
44
|
execution_status: status,
|
|
@@ -38,7 +66,9 @@ export function executeMockStep(step, input, attempt) {
|
|
|
38
66
|
});
|
|
39
67
|
if (step.kind === "approval") {
|
|
40
68
|
const autoApprove = step.approvalSpec?.autoApprove ?? false;
|
|
41
|
-
|
|
69
|
+
const result = autoApprove ? make("SUCCESS", "PROCEED") : make("YIELD_EXTERNAL", "PROCEED");
|
|
70
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
71
|
+
return result;
|
|
42
72
|
}
|
|
43
73
|
if (typeof payload.storyChapter === "number") {
|
|
44
74
|
const chapterNumber = payload.storyChapter;
|
|
@@ -46,13 +76,15 @@ export function executeMockStep(step, input, attempt) {
|
|
|
46
76
|
const paragraph = chapterNumber === 1
|
|
47
77
|
? `A curious bunny set out at sunrise to solve the puzzle in ${prompt}.`
|
|
48
78
|
: `By sunset, the bunny used what it learned to finish ${prompt} with courage and kindness.`;
|
|
49
|
-
|
|
79
|
+
const result = make("SUCCESS", "PROCEED", {
|
|
50
80
|
chapterNumber,
|
|
51
81
|
chapterTitle: `Chapter ${chapterNumber}`,
|
|
52
82
|
paragraph,
|
|
53
83
|
chapterMarkdown: `## Chapter ${chapterNumber}\n\n${paragraph}`,
|
|
54
84
|
storyPrompt: prompt,
|
|
55
85
|
});
|
|
86
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
87
|
+
return result;
|
|
56
88
|
}
|
|
57
89
|
if (payload.validateStory === true) {
|
|
58
90
|
const expectedChapters = Number(payload.requiredChapters ?? 2);
|
|
@@ -64,19 +96,23 @@ export function executeMockStep(step, input, attempt) {
|
|
|
64
96
|
const hasBunnyTheme = /bunny/i.test(chapters);
|
|
65
97
|
const isValid = chapterMatches.length === expectedChapters && hasBunnyTheme;
|
|
66
98
|
if (!isValid) {
|
|
67
|
-
|
|
99
|
+
const result = make("QA_REJECTED", "RETRY_CURRENT", {
|
|
68
100
|
expectedChapters,
|
|
69
101
|
foundChapters: chapterMatches.length,
|
|
70
102
|
hasBunnyTheme,
|
|
71
103
|
validationPassed: false,
|
|
72
104
|
}, `Story must include exactly ${expectedChapters} chapters and bunny-themed content`);
|
|
105
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
106
|
+
return result;
|
|
73
107
|
}
|
|
74
|
-
|
|
108
|
+
const result = make("SUCCESS", "PROCEED", {
|
|
75
109
|
expectedChapters,
|
|
76
110
|
foundChapters: chapterMatches.length,
|
|
77
111
|
hasBunnyTheme,
|
|
78
112
|
validationPassed: true,
|
|
79
113
|
});
|
|
114
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
115
|
+
return result;
|
|
80
116
|
}
|
|
81
117
|
if (payload.renderStoryMarkdown === true) {
|
|
82
118
|
const previousOutput = asRecord(input.step_context.previous_output);
|
|
@@ -84,27 +120,40 @@ export function executeMockStep(step, input, attempt) {
|
|
|
84
120
|
.sort((a, b) => Number(a.chapterNumber ?? 0) - Number(b.chapterNumber ?? 0))
|
|
85
121
|
.map((entry) => String(entry.chapterMarkdown));
|
|
86
122
|
if (chapters.length === 0) {
|
|
87
|
-
|
|
123
|
+
const result = make("QA_REJECTED", "RETRY_CURRENT", { chapterCount: 0 }, "No chapters available to render");
|
|
124
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
125
|
+
return result;
|
|
88
126
|
}
|
|
89
127
|
const title = String(payload.storyTitle ?? input.global_context.global_state.storyRequest ?? "Bunny Story");
|
|
90
128
|
const storyMarkdown = `# ${title}\n\n${chapters.join("\n\n")}`;
|
|
91
|
-
|
|
129
|
+
const result = make("SUCCESS", "PROCEED", {
|
|
92
130
|
chapterCount: chapters.length,
|
|
93
131
|
storyMarkdown,
|
|
94
132
|
});
|
|
133
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
134
|
+
return result;
|
|
95
135
|
}
|
|
136
|
+
let result;
|
|
96
137
|
switch (mockResult) {
|
|
97
138
|
case "retry":
|
|
98
|
-
|
|
139
|
+
result = make("QA_REJECTED", "RETRY_CURRENT");
|
|
140
|
+
break;
|
|
99
141
|
case "rollback":
|
|
100
|
-
|
|
142
|
+
result = make("QA_REJECTED", "ROLLBACK_PREVIOUS");
|
|
143
|
+
break;
|
|
101
144
|
case "restart":
|
|
102
|
-
|
|
145
|
+
result = make("QA_REJECTED", "RESTART_ALL");
|
|
146
|
+
break;
|
|
103
147
|
case "yield":
|
|
104
|
-
|
|
148
|
+
result = make("YIELD_EXTERNAL", "PROCEED");
|
|
149
|
+
break;
|
|
105
150
|
case "fail":
|
|
106
|
-
|
|
151
|
+
result = make("FAILED", "PROCEED");
|
|
152
|
+
break;
|
|
107
153
|
default:
|
|
108
|
-
|
|
154
|
+
result = make("SUCCESS", "PROCEED");
|
|
155
|
+
break;
|
|
109
156
|
}
|
|
157
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
158
|
+
return result;
|
|
110
159
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { InputEnvelope, OutputEnvelope, StepDefinition } from "./types.js";
|
|
1
|
+
import type { InputEnvelope, OutputEnvelope, StepDefinition, StepExecutionHooks } from "./types.js";
|
|
2
2
|
export declare function shouldUseRealOpencode(step: StepDefinition): boolean;
|
|
3
|
-
export declare function executeOpencodeStep(step: StepDefinition, input: InputEnvelope, attempt: number): OutputEnvelope
|
|
3
|
+
export declare function executeOpencodeStep(step: StepDefinition, input: InputEnvelope, attempt: number, hooks?: StepExecutionHooks): Promise<OutputEnvelope>;
|
package/dist/opencodeExecutor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
function asRecord(value) {
|
|
3
3
|
return value && typeof value === "object" ? value : {};
|
|
4
4
|
}
|
|
@@ -13,145 +13,19 @@ export function shouldUseRealOpencode(step) {
|
|
|
13
13
|
const payload = asRecord(step.taskSpec?.payload);
|
|
14
14
|
return payload.useRealAdapter === true && payload.opencodeSmokeTest === true;
|
|
15
15
|
}
|
|
16
|
-
export function executeOpencodeStep(step, input, attempt) {
|
|
16
|
+
export function executeOpencodeStep(step, input, attempt, hooks) {
|
|
17
17
|
const startedAt = Date.now();
|
|
18
18
|
const payload = asRecord(step.taskSpec?.payload);
|
|
19
19
|
const opencodeArgs = Array.isArray(payload.opencodeArgs)
|
|
20
20
|
? payload.opencodeArgs.map((arg) => String(arg))
|
|
21
21
|
: ["--version"];
|
|
22
22
|
const timeoutMs = normalizeTimeout(payload.timeoutMs, 15000);
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
child = spawnSync("opencode", opencodeArgs, {
|
|
26
|
-
encoding: "utf-8",
|
|
27
|
-
timeout: timeoutMs,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
return {
|
|
32
|
-
step_id: step.key,
|
|
33
|
-
execution_status: "FAILED",
|
|
34
|
-
qa_routing: {
|
|
35
|
-
action: "PROCEED",
|
|
36
|
-
feedback_reason: err.message,
|
|
37
|
-
},
|
|
38
|
-
mutated_payload: {
|
|
39
|
-
stepKey: step.key,
|
|
40
|
-
attempt,
|
|
41
|
-
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
42
|
-
realOpencode: true,
|
|
43
|
-
command: "opencode",
|
|
44
|
-
args: opencodeArgs,
|
|
45
|
-
timeoutMs,
|
|
46
|
-
},
|
|
47
|
-
metadata: {
|
|
48
|
-
execution_time_ms: Date.now() - startedAt,
|
|
49
|
-
external_intervention_required: false,
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const stdout = child.stdout ?? "";
|
|
54
|
-
const stderr = child.stderr ?? "";
|
|
55
|
-
const status = typeof child.status === "number" ? child.status : 1;
|
|
56
|
-
const output = `${stdout}\n${stderr}`;
|
|
57
|
-
const expectContains = payload.expectContains ? String(payload.expectContains) : undefined;
|
|
58
|
-
const expectPattern = payload.expectPattern ? String(payload.expectPattern) : undefined;
|
|
59
|
-
const containsExpected = expectContains
|
|
60
|
-
? output.toLowerCase().includes(expectContains.toLowerCase())
|
|
61
|
-
: true;
|
|
62
|
-
let matchesPattern = true;
|
|
63
|
-
if (expectPattern) {
|
|
64
|
-
try {
|
|
65
|
-
matchesPattern = new RegExp(expectPattern).test(output);
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
return {
|
|
69
|
-
step_id: step.key,
|
|
70
|
-
execution_status: "FAILED",
|
|
71
|
-
qa_routing: {
|
|
72
|
-
action: "PROCEED",
|
|
73
|
-
feedback_reason: `Invalid expectPattern regex: ${err.message}`,
|
|
74
|
-
},
|
|
75
|
-
mutated_payload: {
|
|
76
|
-
stepKey: step.key,
|
|
77
|
-
attempt,
|
|
78
|
-
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
79
|
-
realOpencode: true,
|
|
80
|
-
command: "opencode",
|
|
81
|
-
args: opencodeArgs,
|
|
82
|
-
exitStatus: status,
|
|
83
|
-
stdout,
|
|
84
|
-
stderr,
|
|
85
|
-
expectPattern,
|
|
86
|
-
},
|
|
87
|
-
metadata: {
|
|
88
|
-
execution_time_ms: Date.now() - startedAt,
|
|
89
|
-
external_intervention_required: false,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (child.error || status !== 0) {
|
|
95
|
-
return {
|
|
96
|
-
step_id: step.key,
|
|
97
|
-
execution_status: "FAILED",
|
|
98
|
-
qa_routing: {
|
|
99
|
-
action: "PROCEED",
|
|
100
|
-
feedback_reason: child.error?.message ?? `opencode exited with status ${status}`,
|
|
101
|
-
},
|
|
102
|
-
mutated_payload: {
|
|
103
|
-
stepKey: step.key,
|
|
104
|
-
attempt,
|
|
105
|
-
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
106
|
-
realOpencode: true,
|
|
107
|
-
command: "opencode",
|
|
108
|
-
args: opencodeArgs,
|
|
109
|
-
exitStatus: status,
|
|
110
|
-
stdout,
|
|
111
|
-
stderr,
|
|
112
|
-
},
|
|
113
|
-
metadata: {
|
|
114
|
-
execution_time_ms: Date.now() - startedAt,
|
|
115
|
-
external_intervention_required: false,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (!containsExpected || !matchesPattern) {
|
|
120
|
-
const feedback = !containsExpected
|
|
121
|
-
? `Output did not contain expected token: ${expectContains}`
|
|
122
|
-
: `Output did not match expected pattern: ${expectPattern}`;
|
|
123
|
-
return {
|
|
124
|
-
step_id: step.key,
|
|
125
|
-
execution_status: "QA_REJECTED",
|
|
126
|
-
qa_routing: {
|
|
127
|
-
action: "RETRY_CURRENT",
|
|
128
|
-
feedback_reason: feedback,
|
|
129
|
-
},
|
|
130
|
-
mutated_payload: {
|
|
131
|
-
stepKey: step.key,
|
|
132
|
-
attempt,
|
|
133
|
-
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
134
|
-
realOpencode: true,
|
|
135
|
-
command: "opencode",
|
|
136
|
-
args: opencodeArgs,
|
|
137
|
-
exitStatus: status,
|
|
138
|
-
stdout,
|
|
139
|
-
stderr,
|
|
140
|
-
containsExpected,
|
|
141
|
-
matchesPattern,
|
|
142
|
-
},
|
|
143
|
-
metadata: {
|
|
144
|
-
execution_time_ms: Date.now() - startedAt,
|
|
145
|
-
external_intervention_required: false,
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
23
|
+
const makeResult = (status, feedbackReason, extra = {}, action = "PROCEED") => ({
|
|
150
24
|
step_id: step.key,
|
|
151
|
-
execution_status:
|
|
25
|
+
execution_status: status,
|
|
152
26
|
qa_routing: {
|
|
153
|
-
action
|
|
154
|
-
feedback_reason:
|
|
27
|
+
action,
|
|
28
|
+
feedback_reason: feedbackReason,
|
|
155
29
|
},
|
|
156
30
|
mutated_payload: {
|
|
157
31
|
stepKey: step.key,
|
|
@@ -160,15 +34,104 @@ export function executeOpencodeStep(step, input, attempt) {
|
|
|
160
34
|
realOpencode: true,
|
|
161
35
|
command: "opencode",
|
|
162
36
|
args: opencodeArgs,
|
|
163
|
-
|
|
164
|
-
stdout,
|
|
165
|
-
stderr,
|
|
166
|
-
containsExpected,
|
|
167
|
-
matchesPattern,
|
|
37
|
+
...extra,
|
|
168
38
|
},
|
|
169
39
|
metadata: {
|
|
170
40
|
execution_time_ms: Date.now() - startedAt,
|
|
171
41
|
external_intervention_required: false,
|
|
172
42
|
},
|
|
173
|
-
};
|
|
43
|
+
});
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
let child;
|
|
46
|
+
try {
|
|
47
|
+
child = spawn("opencode", opencodeArgs, {
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const result = makeResult("FAILED", err.message, { timeoutMs });
|
|
53
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
54
|
+
resolve(result);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
hooks?.onStarted?.({ command: "opencode", args: opencodeArgs, timeoutMs });
|
|
58
|
+
const outChunks = [];
|
|
59
|
+
const errChunks = [];
|
|
60
|
+
child.stdout?.setEncoding("utf-8");
|
|
61
|
+
child.stderr?.setEncoding("utf-8");
|
|
62
|
+
child.stdout?.on("data", (chunk) => {
|
|
63
|
+
outChunks.push(chunk);
|
|
64
|
+
hooks?.onStdout?.(chunk);
|
|
65
|
+
});
|
|
66
|
+
child.stderr?.on("data", (chunk) => {
|
|
67
|
+
errChunks.push(chunk);
|
|
68
|
+
hooks?.onStderr?.(chunk);
|
|
69
|
+
});
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
timedOut = true;
|
|
73
|
+
child.kill("SIGTERM");
|
|
74
|
+
const stdout = outChunks.join("");
|
|
75
|
+
const stderr = errChunks.join("");
|
|
76
|
+
const result = makeResult("FAILED", `timed out after ${timeoutMs}ms`, { timeoutMs, stdout, stderr });
|
|
77
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status, exitStatus: null, timedOut: true });
|
|
78
|
+
resolve(result);
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
child.on("error", (err) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
if (timedOut) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const stdout = outChunks.join("");
|
|
86
|
+
const stderr = errChunks.join("");
|
|
87
|
+
const result = makeResult("FAILED", err.message, { timeoutMs, stdout, stderr });
|
|
88
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status });
|
|
89
|
+
resolve(result);
|
|
90
|
+
});
|
|
91
|
+
child.on("close", (code) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
if (timedOut) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const stdout = outChunks.join("");
|
|
97
|
+
const stderr = errChunks.join("");
|
|
98
|
+
const exitStatus = code ?? 1;
|
|
99
|
+
const output = `${stdout}\n${stderr}`;
|
|
100
|
+
const expectContains = payload.expectContains ? String(payload.expectContains) : undefined;
|
|
101
|
+
const expectPattern = payload.expectPattern ? String(payload.expectPattern) : undefined;
|
|
102
|
+
const containsExpected = expectContains
|
|
103
|
+
? output.toLowerCase().includes(expectContains.toLowerCase())
|
|
104
|
+
: true;
|
|
105
|
+
let matchesPattern = true;
|
|
106
|
+
if (expectPattern) {
|
|
107
|
+
try {
|
|
108
|
+
matchesPattern = new RegExp(expectPattern).test(output);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const result = makeResult("FAILED", `Invalid expectPattern regex: ${err.message}`, { exitStatus, stdout, stderr, expectPattern });
|
|
112
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status, exitStatus });
|
|
113
|
+
resolve(result);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (exitStatus !== 0) {
|
|
118
|
+
const result = makeResult("FAILED", `opencode exited with status ${exitStatus}`, { exitStatus, stdout, stderr });
|
|
119
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status, exitStatus });
|
|
120
|
+
resolve(result);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!containsExpected || !matchesPattern) {
|
|
124
|
+
const feedback = !containsExpected
|
|
125
|
+
? `Output did not contain expected token: ${expectContains}`
|
|
126
|
+
: `Output did not match expected pattern: ${expectPattern}`;
|
|
127
|
+
const result = makeResult("QA_REJECTED", feedback, { exitStatus, stdout, stderr, containsExpected, matchesPattern }, "RETRY_CURRENT");
|
|
128
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status, exitStatus });
|
|
129
|
+
resolve(result);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const result = makeResult("SUCCESS", "", { exitStatus, stdout, stderr, containsExpected, matchesPattern });
|
|
133
|
+
hooks?.onFinished?.({ executionStatus: result.execution_status, exitStatus });
|
|
134
|
+
resolve(result);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
174
137
|
}
|