@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
package/README.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # workflow-manager
2
2
 
3
- CLI runner for in-memory and Markdown/JSON workflow orchestration.
3
+ **wfm** (workflow-manager) is a CLI orchestrator that runs multi-step workflows where the steps are executed by AI coding agents, with human approval gates in between. You define a workflow in a Markdown (YAML frontmatter) or JSON file, and `wfm run` executes it step by step — think of it as a CI pipeline for AI agents: declarative multi-agent orchestration with QA routing, approval gates, and a sharing registry, run from a single binary.
4
+
5
+ The core ideas:
6
+
7
+ - **Workflow definitions.** A workflow is a list of steps with stable keys, objectives, and `dependsOn` edges (the engine topologically orders them). Each step is one of three kinds: `task` (delegated to an agent), `approval` (a human checkpoint), or `system`. Steps can carry retry policies, timeouts, and validation rules (`human`, `external`, or `none`).
8
+ - **Agent adapters.** Each task step picks an adapter that does the actual work: `pi-agent` (the default — drives the `pi` coding agent CLI with a built prompt plus `input.json`/`output.json` envelopes), `claude-code` (pipes a built prompt into the `claude` CLI), `opencode`, `codex` (not yet implemented, mock-routed), and `mock` (deterministic simulator for tests/authoring). Steps can declare skills, MCP endpoints, system prompts, and a model under `taskSpec.init`, which get injected into the agent's prompt or input file.
9
+ - **The envelope protocol.** Every step execution returns a structured result: an execution status (`SUCCESS`, `FAILED`, `QA_REJECTED`, `YIELD_EXTERNAL`) plus a QA routing action (`PROCEED`, `RETRY_CURRENT`, `ROLLBACK_PREVIOUS`, `RESTART_ALL`). That's what lets the engine retry a step, roll back to the previous one, or restart the whole run based on what the agent reported.
10
+ - **Human-in-the-loop control.** While a run is active, wfm starts a local HTTP attach API (token-protected, with SSE event streaming), so a waiting step can be resolved either in the terminal prompt or from another shell with `wfm approve` / `wfm resume` / `wfm cancel`.
11
+ - **A registry.** `wfm publish` / `pull` / `search` / `auth` talk to a Supabase-backed remote registry (the `apps/remote-registry` web app in this repo) for sharing workflows, with skills bundled and SHA-256 verified. Runs also emit opt-in telemetry there.
12
+ - **Supporting commands.** `wfm scaffold` writes a starter workflow, `wfm validate` checks the schema and dependency cycles, `wfm doctor` verifies the host has the needed CLIs and API keys before a run, `wfm agent` drops an AGENTS.md rules file, and `wfm man` shows the man page.
13
+
14
+ Install the latest prebuilt CLI with:
15
+
16
+ ```bash
17
+ curl -fsSL https://github.com/navio/workflow-manager/releases/latest/download/workflow-manager-installer.sh | bash
18
+ ```
19
+
20
+ If `wfm` is not available immediately in the same terminal, run the shell reload command printed by the installer or open a new shell, then run `wfm --help`.
4
21
 
5
22
  ## What it does
6
23
 
@@ -8,15 +25,17 @@ CLI runner for in-memory and Markdown/JSON workflow orchestration.
8
25
  - Validates structure, dependencies, adapters, and validation modes
9
26
  - Executes workflow steps with deterministic run state transitions
10
27
  - Emits a full event timeline and JSON run result
28
+ - Starts a local attach API for live run snapshots and SSE events
11
29
  - Records authenticated CLI run telemetry for success, failure, and workflow effectiveness
12
30
  - Publishes and pulls shared workflows from the remote registry
13
31
 
14
32
  ## Architecture
15
33
 
16
- - `src/index.ts`: CLI commands (`questions`, `scaffold`, `validate`, `run`)
34
+ - `src/index.ts`: CLI commands (`doctor`, `agent`, `scaffold`, `validate`, `run`)
17
35
  - `src/parser.ts`: parsing + validation
18
36
  - `src/engine.ts`: execution loop, confirmations, retries, rollback/restart
19
- - `src/mockExecutor.ts`: mock step executor for simulation
37
+ - `src/piAgentExecutor.ts`: default executor driving the `pi` coding agent CLI
38
+ - `src/mockExecutor.ts`: explicit mock step executor for simulation
20
39
  - `src/events.ts`: event sequencing/logging
21
40
  - `src/types.ts`: contracts and status enums
22
41
  - `apps/remote-registry/`: React + Vite remote registry app
@@ -31,9 +50,13 @@ bun install
31
50
  bun run build
32
51
  bun link
33
52
 
53
+ wfm doctor
54
+ wfm agent ./AGENTS.md
34
55
  wfm scaffold ./example-workflow.md
35
56
  wfm validate ./example-workflow.md
57
+ wfm doctor ./example-workflow.md
36
58
  wfm run ./example-workflow.md --auto-confirm-all
59
+ wfm run ./example-workflow.md --auto-confirm-all --verbose
37
60
 
38
61
  # JSON workflow support
39
62
  wfm scaffold ./example-workflow.json --format json
@@ -47,13 +70,58 @@ wfm publish ./example-workflow.json --visibility public --tag storytelling,examp
47
70
  wfm pull alice/remote-bunny --output ./remote-bunny.json
48
71
  ```
49
72
 
73
+ Task steps run with the `pi-agent` adapter by default when `taskSpec.adapterKey` is omitted. The runner drives the [pi coding agent](https://github.com/earendil-works/pi-mono) CLI (`pi`) in non-interactive print mode: it builds a prompt from the step objective, workflow context, previous step output, and `taskSpec.init` context, and maps `taskSpec.init.model` to `--model`, each `taskSpec.init.systemPrompts` entry to `--append-system-prompt`, and each resolved skill to a `--skill` file written into the run directory. The full structured step input is also written to `input.json` in the run directory, and the agent is asked to write a result envelope to `output.json` for QA routing. If the agent exits successfully without writing one, the step succeeds and its response text becomes the step output. Set `WFM_PI_AGENT_COMMAND=/path/to/pi` to override the default `pi` binary, or set `taskSpec.payload.command` and `taskSpec.payload.args` for a specific step; custom commands receive the input/output file paths in the `WFM_PI_INPUT_FILE` and `WFM_PI_OUTPUT_FILE` environment variables (previous releases passed `--input`/`--output` flags instead). Use `adapterKey: mock` explicitly for deterministic simulation workflows.
74
+
75
+ Before execution starts, `wfm run` validates host runtime access for real adapters. Default `pi-agent` steps require the configured `pi` command to be installed and executable; pi manages provider credentials in its own auth store, so no API key environment variables are inferred for pi steps. Real `opencode` steps require the `opencode` CLI, and real `claude-code` steps require the `claude` CLI. For those adapters, if `taskSpec.init.model` or `taskSpec.payload.model` identifies a known provider, the matching key must also be present: `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, or `ANTHROPIC_API_KEY`. For custom clients, set `taskSpec.payload.requiredEnv` to the environment variable names that must exist before the run can start.
76
+
77
+ Use `wfm doctor` to inspect host adapter setup and `wfm doctor <workflow>` to validate a workflow's runtime requirements before running it. Today, `pi-agent` is the real default host adapter (driving the `pi` CLI), `mock` is a deterministic simulator, `opencode` and `claude-code` have opt-in real host paths, and `codex` is still mock-routed until a real executor is implemented.
78
+
79
+ During `wfm run`, the CLI starts a local attach API on `127.0.0.1`. Use `--port <n>` to bind a fixed port or omit it to let the OS choose one. The CLI prints the attach base URL and bearer token before execution starts.
80
+
81
+ By default, `wfm run` now prints live workflow progress to stderr with the current step, elapsed workflow time, and remaining step count. Pass `--verbose` to stream per-step agent output chunks and execution status updates while the workflow is running. When a human approval is required in an interactive terminal, the CLI now prints an approval summary and prompts for approve or cancel inline.
82
+
83
+ Runner API endpoints include:
84
+
85
+ - `GET /session`
86
+ - `GET /runs/:runId`
87
+ - `GET /runs/:runId/steps/:stepKey`
88
+ - `GET /runs/:runId/logs`
89
+ - `GET /runs/:runId/events` (SSE)
90
+ - `POST /runs/:runId/approve`
91
+ - `POST /runs/:runId/resume`
92
+ - `POST /runs/:runId/cancel`
93
+
94
+ Local control helpers are available too:
95
+
96
+ ```bash
97
+ wfm approve --url http://127.0.0.1:43121 --token <token> --step review --actor alice --note "LGTM"
98
+ wfm cancel --url http://127.0.0.1:43121 --token <token> --step review --actor alice --note "stop this run"
99
+ ```
100
+
101
+ See `doc/guide/runner-api.md` for the full contract.
102
+
103
+ Prefer the release binary instead of a source checkout:
104
+
105
+ ```bash
106
+ curl -fsSL https://github.com/navio/workflow-manager/releases/latest/download/workflow-manager-installer.sh | bash
107
+ ```
108
+
109
+ If `wfm` is not available immediately in the same terminal, run the shell reload command printed by the installer or open a new shell, then run `wfm --help`.
110
+
50
111
  ## Build
51
112
 
52
113
  ```bash
114
+ bun run lint
53
115
  bun run build
54
116
  bun test
55
117
  ```
56
118
 
119
+ Apply safe lint fixes:
120
+
121
+ ```bash
122
+ bun run lint:fix
123
+ ```
124
+
57
125
  Build a standalone Bun binary:
58
126
 
59
127
  ```bash
@@ -104,16 +172,41 @@ bun run docs:build
104
172
  bun run docs:preview
105
173
  ```
106
174
 
107
- Docs are now treated as a manual Netlify release flow. Build `doc/.vitepress/dist` locally and deploy it manually when needed.
175
+ Docs can be deployed from `doc/.vitepress/dist` locally, and GitHub Pages publishes them from `main` when files under `doc/` change.
108
176
 
109
177
  Remote registry app:
110
178
 
111
179
  ```bash
112
180
  bun run remote-registry:dev
181
+ bun --cwd apps/remote-registry lint
182
+ bun run remote-registry:test
183
+ bun run remote-registry:test:auth:local
184
+ bun run remote-registry:test:publish:local
185
+ bun run remote-registry:test:smoke:local
113
186
  bun run remote-registry:build
114
187
  ```
115
188
 
116
- `workflow-manager-ui` is the Netlify auto-release target. The root `netlify.toml` now builds `apps/remote-registry/` so connected Netlify Git deploys can produce preview deploys for PRs and production deploys from merges to `main`.
189
+ Pre-commit hooks run staged-file linting automatically after `bun install` via the repo `prepare` script and `lint-staged`.
190
+
191
+ Supabase local validation:
192
+
193
+ ```bash
194
+ bun run supabase:start
195
+ bun run supabase:db:reset
196
+ bun run supabase:db:lint
197
+ bun run supabase:test
198
+ bun run supabase:stop
199
+ ```
200
+
201
+ GitHub Actions now owns the production Supabase release flow:
202
+
203
+ - `.github/workflows/supabase-validate.yml` validates `supabase/**` changes on pull requests
204
+ - `.github/workflows/supabase-release.yml` applies migrations and deploys Edge Functions after merge to `main`
205
+ - configure repository secrets: `SUPABASE_ACCESS_TOKEN`, `SUPABASE_DB_PASSWORD`, and `SUPABASE_PROJECT_ID`
206
+ `remote-registry:test:auth:local` is an opt-in live smoke that validates signup, email confirmation, signin, and password-reset email against a local Supabase stack (`supabase start`) and Mailpit (`http://127.0.0.1:54324`).
207
+ `remote-registry:test:publish:local` is an opt-in live smoke that validates handle claim and publish/search/pull owner-slug retrieval against the same local stack.
208
+
209
+ The docs site is published at `https://navio.github.io/workflow-manager/` via `.github/workflows/deploy-docs.yml`.
117
210
 
118
211
  Manual help:
119
212
 
@@ -121,6 +214,12 @@ Manual help:
121
214
  wfm man
122
215
  ```
123
216
 
217
+ Agent rules:
218
+
219
+ ```bash
220
+ wfm agent ./AGENTS.md
221
+ ```
222
+
124
223
  Remote registry commands:
125
224
 
126
225
  ```bash
@@ -145,6 +244,14 @@ npx @tanstack/intent@latest list
145
244
  npx @tanstack/intent@latest install
146
245
  ```
147
246
 
247
+ Workflow skill resolution now follows a local-authoring + portable-artifact model:
248
+
249
+ - authoring workflows can point to local skill files under `./skills/**/SKILL.md`
250
+ - `workflow-manager publish` inlines skill markdown into `skills[*].content`
251
+ - publish also writes `skills[*].contentSha256` for integrity checks
252
+ - pulled workflows are rejected if any declared skill is missing embedded content
253
+ - optional `skills[*].upstream` metadata can record source repo/ref/path for auditability
254
+
148
255
  See `skills/README.md` for the packaged skill layout, TanStack Intent integration, and release checks.
149
256
 
150
257
  The deployed registry dashboard also supports browser-based token creation, workflow publishing, and creator analytics.
@@ -164,25 +271,29 @@ Current dashboard capabilities include:
164
271
  - Update docs under `doc/` when changing schema or runtime behavior
165
272
  - Add or update tests in `tests/` when touching parser or engine logic
166
273
 
167
- Netlify auto-release is configured for the UI in `netlify.toml`:
274
+ Netlify is reserved for `workflow-manager-ui`.
168
275
 
169
- - base directory: `apps/remote-registry`
170
- - build command: `bun run build`
171
- - publish directory: `dist`
172
- - PRs: Deploy Previews
173
- - `main`: Production deploys for `workflow-manager-ui`
276
+ - build command: `bun run netlify:build`
277
+ - publish directory: `.netlify/deploy`
278
+ - `workflow-manager-ui` must set `NETLIFY_SITE_TARGET=remote-ui`
174
279
 
175
- The docs site is no longer the auto-release target and should be deployed manually.
280
+ GitHub Pages is reserved for docs and only deploys on changes under `doc/`.
176
281
 
177
282
  ## Release
178
283
 
179
284
  - Push a semantic tag like `v0.2.0` to trigger the GitHub Actions release workflow.
285
+ - Merges to `main` that change packaged CLI files (`src/`, `skills/`, `man/`, `package.json`, `tsconfig.json`) update the release PR maintained by `.github/workflows/release-please.yml`.
286
+ - Release Please uses Conventional Commit semantics to propose the next npm version: `fix:` -> patch, `feat:` -> minor, and `!` or `BREAKING CHANGE` -> major.
287
+ - Merging the Release Please PR bumps `package.json`, updates `CHANGELOG.md`, triggers `.github/workflows/npm-publish.yml` to publish `@workflow-manager/runner` to npm, and creates the release tag consumed by `.github/workflows/release.yml`.
288
+ - Docs, remote-registry, and other non-package changes do not trigger release automation.
289
+ - Configure the repository `NPM_TOKEN` secret with an npm automation token that can publish `@workflow-manager/runner`.
180
290
  - Publish the root npm package when you want the CLI runner and `skills/` bundle to ship together.
181
291
  - The workflow runs tests and build, then compiles binaries for:
182
292
  - macOS arm64: `wfm-macos-arm64`
183
293
  - Linux x64: `wfm-linux-x64`
184
294
  - Windows x64: `wfm-windows-x64.exe`
185
295
  - Assets are attached to the GitHub Release for that tag.
296
+ - Each release also includes `workflow-manager-installer.sh`, so users can install `wfm` with `curl -fsSL https://github.com/navio/workflow-manager/releases/latest/download/workflow-manager-installer.sh | bash`.
186
297
 
187
298
  ## Documentation
188
299
 
@@ -0,0 +1,4 @@
1
+ import type { AdapterKey } from "./types.js";
2
+ export declare const DEFAULT_TASK_ADAPTER: AdapterKey;
3
+ export declare const SUPPORTED_ADAPTERS: readonly AdapterKey[];
4
+ export declare function resolveTaskAdapter(adapter?: AdapterKey): AdapterKey;
@@ -0,0 +1,11 @@
1
+ export const DEFAULT_TASK_ADAPTER = "pi-agent";
2
+ export const SUPPORTED_ADAPTERS = [
3
+ "pi-agent",
4
+ "mock",
5
+ "opencode",
6
+ "codex",
7
+ "claude-code",
8
+ ];
9
+ export function resolveTaskAdapter(adapter) {
10
+ return adapter ?? DEFAULT_TASK_ADAPTER;
11
+ }
@@ -0,0 +1,4 @@
1
+ import type { InputEnvelope, OutputEnvelope, StepDefinition, StepExecutionHooks, WorkflowDefinition } from "./types.js";
2
+ export declare function normalizeTimeout(value: unknown, fallbackMs?: number): number;
3
+ export declare function shouldUseRealClaudeCode(step: StepDefinition): boolean;
4
+ export declare function executeClaudeCodeStep(step: StepDefinition, input: InputEnvelope, attempt: number, workflow?: WorkflowDefinition, workflowFilePath?: string, hooks?: StepExecutionHooks): Promise<OutputEnvelope>;
@@ -0,0 +1,173 @@
1
+ import { spawn } from "node:child_process";
2
+ import { resolveSkill } from "./skillResolver.js";
3
+ function asRecord(value) {
4
+ return value && typeof value === "object" ? value : {};
5
+ }
6
+ const previousOutputTextKeys = ["output", "storyMarkdown", "chapterMarkdown", "stdout"];
7
+ function previousOutputTextSections(value) {
8
+ const record = asRecord(value);
9
+ const sections = [];
10
+ for (const key of previousOutputTextKeys) {
11
+ const text = record[key];
12
+ if (typeof text === "string" && text.trim()) {
13
+ sections.push(text);
14
+ }
15
+ }
16
+ if (sections.length > 0) {
17
+ return sections;
18
+ }
19
+ for (const [key, value] of Object.entries(record)) {
20
+ if (["stepKey", "attempt", "adapter", "command", "exitStatus", "mockResult"].includes(key))
21
+ continue;
22
+ if (typeof value === "string" && value.trim()) {
23
+ sections.push(`${key}: ${value}`);
24
+ }
25
+ }
26
+ return sections;
27
+ }
28
+ export function normalizeTimeout(value, fallbackMs = 120000) {
29
+ const timeout = Number(value ?? fallbackMs);
30
+ if (!Number.isFinite(timeout) || timeout <= 0)
31
+ return fallbackMs;
32
+ return Math.floor(timeout);
33
+ }
34
+ function buildPrompt(step, input, workflow, workflowFilePath) {
35
+ const payload = asRecord(step.taskSpec?.payload);
36
+ if (typeof payload.prompt === "string" && payload.prompt.trim()) {
37
+ return payload.prompt;
38
+ }
39
+ const parts = [];
40
+ const systemPrompts = input.priming_configuration.system_prompts;
41
+ if (systemPrompts.length > 0) {
42
+ parts.push(systemPrompts.join("\n"));
43
+ }
44
+ const skills = input.priming_configuration.required_skills;
45
+ if (skills.length > 0) {
46
+ const resolvedNames = [];
47
+ for (const name of skills) {
48
+ const resolved = workflow && workflowFilePath ? resolveSkill(name, workflow, workflowFilePath) : null;
49
+ if (resolved) {
50
+ parts.push(resolved.content);
51
+ }
52
+ else {
53
+ resolvedNames.push(name);
54
+ }
55
+ }
56
+ if (resolvedNames.length > 0) {
57
+ parts.push(`Apply the following skills: ${resolvedNames.join(", ")}`);
58
+ }
59
+ }
60
+ // Inject primitive user inputs (feature, ticket, etc.) — skip step output objects.
61
+ // Newlines stripped from values to reduce prompt injection surface.
62
+ const globalState = input.global_context.global_state;
63
+ const inputLines = Object.entries(globalState)
64
+ .filter(([, v]) => typeof v === "string" || typeof v === "number")
65
+ .map(([k, v]) => `${k}: ${String(v).replace(/[\n\r]/g, " ")}`);
66
+ if (inputLines.length > 0) {
67
+ parts.push(`Input:\n${inputLines.join("\n")}`);
68
+ }
69
+ parts.push(input.step_context.step_objective);
70
+ // Inject output from previous steps so context flows forward
71
+ const prev = input.step_context.previous_output;
72
+ for (const [key, val] of Object.entries(prev)) {
73
+ const sections = previousOutputTextSections(val);
74
+ if (sections.length > 0) {
75
+ parts.push(`Output from ${key}:\n${sections.join("\n\n")}`);
76
+ }
77
+ }
78
+ const context = input.priming_configuration.context;
79
+ if (typeof context === "string" && context.trim()) {
80
+ parts.push(`Context:\n${context}`);
81
+ }
82
+ else if (context && typeof context === "object") {
83
+ const str = JSON.stringify(context, null, 2);
84
+ if (str !== "{}")
85
+ parts.push(`Context:\n${str}`);
86
+ }
87
+ return parts.join("\n\n");
88
+ }
89
+ export function shouldUseRealClaudeCode(step) {
90
+ const payload = asRecord(step.taskSpec?.payload);
91
+ return payload.useRealAdapter === true;
92
+ }
93
+ export function executeClaudeCodeStep(step, input, attempt, workflow, workflowFilePath, hooks) {
94
+ const startedAt = Date.now();
95
+ const payload = asRecord(step.taskSpec?.payload);
96
+ const timeoutMs = normalizeTimeout(payload.timeoutMs);
97
+ const prompt = buildPrompt(step, input, workflow, workflowFilePath);
98
+ const configuredModel = typeof input.priming_configuration.model === "string" && input.priming_configuration.model.trim()
99
+ ? input.priming_configuration.model
100
+ : typeof payload.model === "string" && payload.model.trim()
101
+ ? payload.model
102
+ : undefined;
103
+ const args = ["-p"];
104
+ if (configuredModel) {
105
+ args.push("--model", configuredModel);
106
+ }
107
+ const makeResult = (status, reason, extra = {}) => ({
108
+ step_id: step.key,
109
+ execution_status: status,
110
+ qa_routing: { action: "PROCEED", feedback_reason: reason },
111
+ mutated_payload: { stepKey: step.key, attempt, adapter: "claude-code", prompt, model: configuredModel, ...extra },
112
+ metadata: { execution_time_ms: Date.now() - startedAt, external_intervention_required: false },
113
+ });
114
+ return new Promise((resolve) => {
115
+ let child;
116
+ try {
117
+ child = spawn("claude", args);
118
+ }
119
+ catch (err) {
120
+ resolve(makeResult("FAILED", err.message));
121
+ return;
122
+ }
123
+ hooks?.onStarted?.({ command: "claude", args, model: configuredModel });
124
+ child.stdin?.on("error", () => undefined);
125
+ child.stdin?.end(prompt);
126
+ const outChunks = [];
127
+ const errChunks = [];
128
+ child.stdout?.on("data", (chunk) => {
129
+ const text = chunk.toString();
130
+ outChunks.push(text);
131
+ hooks?.onStdout?.(text);
132
+ });
133
+ child.stderr?.on("data", (chunk) => {
134
+ const text = chunk.toString();
135
+ errChunks.push(text);
136
+ hooks?.onStderr?.(text);
137
+ });
138
+ let timedOut = false;
139
+ const timer = setTimeout(() => {
140
+ timedOut = true;
141
+ child.kill("SIGTERM");
142
+ const result = makeResult("FAILED", `timed out after ${timeoutMs}ms`);
143
+ hooks?.onFinished?.({ executionStatus: result.execution_status, timedOut: true });
144
+ resolve(result);
145
+ }, timeoutMs);
146
+ child.on("error", (err) => {
147
+ clearTimeout(timer);
148
+ if (timedOut)
149
+ return;
150
+ const result = makeResult("FAILED", err.message);
151
+ hooks?.onFinished?.({ executionStatus: result.execution_status });
152
+ resolve(result);
153
+ });
154
+ child.on("close", (code) => {
155
+ clearTimeout(timer);
156
+ if (timedOut)
157
+ return;
158
+ const stdout = outChunks.join("");
159
+ const stderr = errChunks.join("");
160
+ const exitStatus = code ?? 1;
161
+ if (exitStatus !== 0) {
162
+ const result = makeResult("FAILED", `claude exited ${exitStatus}: ${stderr.trim()}`, { exitStatus, stdout, stderr });
163
+ hooks?.onFinished?.({ executionStatus: result.execution_status, exitStatus });
164
+ resolve(result);
165
+ }
166
+ else {
167
+ const result = makeResult("SUCCESS", "", { exitStatus, output: stdout.trim() });
168
+ hooks?.onFinished?.({ executionStatus: result.execution_status, exitStatus });
169
+ resolve(result);
170
+ }
171
+ });
172
+ });
173
+ }
@@ -0,0 +1,36 @@
1
+ import type { RunEvent, RunObserver, RunSnapshot, RunnerLogChunk, StepDetailSnapshot, WorkflowDefinition } from "./types.js";
2
+ interface CliRunRendererOptions {
3
+ workflow: WorkflowDefinition;
4
+ verbose: boolean;
5
+ stream?: NodeJS.WriteStream;
6
+ heartbeatMs?: number;
7
+ }
8
+ export declare class CliRunRenderer implements RunObserver {
9
+ private readonly verbose;
10
+ private readonly stream;
11
+ private readonly heartbeatMs;
12
+ private readonly steps;
13
+ private readonly lineBuffers;
14
+ private lastSnapshot;
15
+ private lastStepDetails;
16
+ private heartbeat;
17
+ private promptPauseCount;
18
+ private started;
19
+ private closed;
20
+ constructor(options: CliRunRendererOptions);
21
+ onSnapshot(snapshot: RunSnapshot, stepDetails: StepDetailSnapshot[]): void;
22
+ onEvent(event: RunEvent): void;
23
+ onLog(log: RunnerLogChunk): void;
24
+ close(): void;
25
+ pauseHeartbeat(): void;
26
+ resumeHeartbeat(): void;
27
+ private renderStepStatus;
28
+ private stepSummary;
29
+ private remainingText;
30
+ private syncHeartbeat;
31
+ private stopHeartbeat;
32
+ private renderHeartbeat;
33
+ private flushBuffers;
34
+ private write;
35
+ }
36
+ export {};