# workflow-mcp

Local GitHub Actions workflow runner with MCP server support. Parses workflow YAML files and executes jobs sequentially on macOS, with support for arbitrary runner-keyed commands, worktrees, and fix-loop recycling.

## Install

```bash
npm install -g @agimon-ai/workflow-mcp
```

## MCP Server

```bash
workflow-mcp mcp-serve
```

Add to Claude Code configuration:

```json
{
  "mcpServers": {
    "workflow-mcp": {
      "command": "npx",
      "args": ["workflow-mcp", "mcp-serve"]
    }
  }
}
```

## Quick start

```bash
workflow-mcp run-workflow .github/workflows/development.yml -p "Add a health check"
```

Stop a running workflow through the registry:

```bash
workflow-mcp stop-workflow <run-key> --workspace default --run-id <run-id>
```

Recover a failed workflow from its active repair or recorded failed job:

```bash
workflow-mcp recover-workflow <run-key> --workspace default
```

## Terminal stops

Terminal workflow stops are restricted to repair handling. A workflow can define a dedicated repair triage phase, including its own prompt and runner-specific commands:

```yaml
repair:
  runs-on: ubuntu-latest
  system-prompt: |
    Determine whether the active repair can proceed.
  steps:
    - name: Triage repair
      interactiveRun:
        claude: ./run-claude-triage.sh "$JOB_SYSTEM_PROMPT"
        codex: ./run-codex-triage.sh "$JOB_SYSTEM_PROMPT"
```

The repair triage can stop the workflow by writing:

```markdown
---
status: stop
---

Explain which required dependency is unavailable and how to unblock it.
```

If `repair` is not defined, the runner injects the archived fix content and terminal-stop instructions into the retried repair job. A normal first-pass job cannot stop the workflow. If it writes `status: stop`, the runner ignores the terminal status and processes the request through the normal repair loop.

## Forward progress inside a repair

A repair replays every job from its restart target through the job that raised the fix. A job that has finished the fix can skip the ones with nothing left to do by writing `$WORKFLOW_RUN_DIR/proceed.md`:

```markdown
---
proceed-to: verification
---

Fix applied and unit-tested; the integration job is unaffected.
```

The target must come after the current job and no later than the job that raised the fix, so that job always gets to judge the result. Skipped jobs are recorded as `skipped` and their outputs are not refreshed, so skip a job only when the fix cannot change what it produces. A request the runner cannot honor is discarded and the run continues job by job; `fix.md` takes precedence when a job writes both.

## Embedded host integration

The package exposes `createEmbeddedWorkflowFeature()` for hosts that need to register workflow tools without owning the stdio MCP transport. Host integrations are responsible for session scoping, UI behavior, and tool activation.

The embedded feature provides workflow discovery, launch, status, stop, recovery, and unified pause/resume/stop control.

| Tool               | Purpose                                                        |
| ------------------ | -------------------------------------------------------------- |
| `list_workflows`   | Read the workflow catalog and descriptions                     |
| `launch_workflow`  | Start a run (enforces the per-session concurrency ceiling)     |
| `workflow_runs`    | List tracked runs and their stages                             |
| `follow_workflow`  | Pin a run's live output above the editor                       |
| `tail_workflow`    | Read a run's recent output once                                |
| `open_workflow`    | Open the run panel over Pi, with the keyboard wired to the run |
| `stop_workflow`    | Request a graceful shutdown                                    |
| `control_workflow` | Request pause, resume, or graceful shutdown                    |
| `recover_workflow` | Resume a failed run from its recovery point                    |

Runs execute inside the Pi process with their output piped, so a launcher never writes over the TUI and never blocks the event loop.

### Runs belong to the session that launched them

Live-run tools are scoped to the Pi session that launched the run, matched on the `PI_SESSION_ID` stamped into the run environment at launch. A run started by another agent session, or from the command line, does not appear in passive UI and cannot be followed, tailed, opened, paused, resumed, or stopped from this session. Asking those actions for one by key is refused exactly as an unknown key is, so probing tells the agent nothing.

The reason is that the registry is a single directory under `$HOME` shared by every repository and every session on the machine. Unscoped live controls would let each agent halt work it knew nothing about, and no agent could tell which runs it was actually responsible for.

Recovery is the deliberate exception: the recovery picker can select any terminal failed run, including one whose original Pi session has closed. Pi exposes that record only through the terminal-only, read-only `workflow_run` action `recovery-evidence`; generic status/tail, launcher scraping, and all live controls remain session-owned. Recovering atomically claims the exact failed record, verifies the adopted capability again during promotion, then transfers ownership by stamping only the replay's `PI_SESSION_ID`; a delegated CLI adopts the registry-issued claim rather than accepting a caller-chosen session. Abandoned claims are reclaimable, while a live adopted recovery stays protected. Recovery dry-runs are registry-read-only. Running and completed records remain ineligible.

The CLI keeps the global view. It is where runs belonging to no live session are managed:

```bash
workflow-mcp list-workflow-statuses --reconcile
workflow-mcp stop-workflow <run-key> --workspace default --run-id <run-id>
```

When a Pi session closes, a run hosted in tmux or cmux keeps going and stays manageable from the CLI. A run without a `launch-command` executes inside the Pi process itself, so it is interrupted and its record finalized as interrupted on the way out, rather than being left as a `running` record behind a pid that no longer exists.

Set `WORKFLOW_MCP_MODE=on` to start with workflow mode already on. Non-interactive harnesses that steer an agent at `launch_workflow` need this, because there is no human present to type `/workflow`.

### The run panel

`open_workflow` puts a run over Pi as a persistent panel: the job tree on top, recent launcher output below, and terminal input forwarded to the run's own agent. A single Escape still reaches the run so you can interrupt it mid-step. The panel handles its own view controls before terminal forwarding, so those cannot be swallowed by the run's TTY. It sizes itself to 80% of the terminal and always keeps its header and footer, so the exit hint survives on a short window.

Focus, not lifetime, is what you toggle. Backgrounding the panel leaves it on screen and sends your typing back to Pi. With no run panel or pinned follow view active, the same focus shortcut opens the workflow inspector. Use the arrow keys or `j`/`k` to choose a workflow, Enter to open its interactive run panel, and Escape to close the inspector.

| Action                                | macOS              | Linux and Windows         |
| ------------------------------------- | ------------------ | ------------------------- |
| Close the panel (the run keeps going) | `Esc Esc` or `⌃⌥Q` | `Esc Esc` or `Ctrl+Alt+Q` |
| Move typing between the run and Pi    | `⌃⌥W`              | `Ctrl+Alt+W`              |
| Interrupt the run's agent mid-step    | `Esc`              | `Esc`                     |

**Pressing Escape twice quickly always closes the panel.** The chords are registered identically on every platform and only their labels differ, but they need Option to send Meta on macOS, a setting in both Terminal.app and iTerm. Without it, `⌃⌥W` arrives as a plain `Ctrl+W` and is forwarded to the run instead, which used to leave the panel with no way out. Escape is the one key every terminal sends the same way, so it is the exit that cannot be taken away.

The first Escape is still delivered to the run, so interrupting a step costs nothing; only a second Escape within 350ms closes the panel. The tradeoff is that the run can no longer be sent two Escapes in quick succession. Pause between them if you need that.

A run hosted natively rather than in cmux or tmux has no terminal to type into, so its panel opens **view only**: it never takes the keyboard, and `⌃⌥W` says so rather than handing your typing to a run that cannot receive it.

Without a UI (`ctx.hasUI` false), `open_workflow` falls back to bringing the launcher to the foreground in cmux or tmux.

### Progress row

While anything is running, one compact line per workflow appears under the editor:

```
● funny-dancing-dog-today    source-tiktok · Source TikTok
● deploy-api                 verify · Run integration tests
```

The display name stays on the left and the current job plus active step stays on the right. Before a step is recorded, the row shows the current phase or `starting`. Long labels are shortened to the terminal width, and the widget disappears when the last active workflow finishes.

### Step reports

Each step transition is appended to the session transcript without starting a turn:

```
[funny-dancing-dog-today]: verify-dispatch
STARTED::Record dispatch evidence

[funny-dancing-dog-today]: verify-dispatch
FINISHED::12s::Record dispatch evidence

[funny-dancing-dog-today]: build
FAILED::4m03s::nx build api
```

The first line identifies the run display name and job. STARTED has no duration because the step has not elapsed yet. FINISHED and FAILED include the measured duration. Lines are held back while the agent is mid-turn and emitted on the next poll, so nothing is lost and nothing interrupts a reply in progress. They do occupy context on later turns.

Step reports, completion notices, and the progress row are scoped to the launching session on the same `PI_SESSION_ID` stamp as the tools, so a session narrates only its own runs into its own transcript.

### Failures

When a run ends in error, the agent is told which job and which step it died on, plus the error text, and is handed a short list of routes forward to put to you rather than choosing one itself: read the output, recover from the recovery point, or report and stop. A run you stopped yourself is reported as stopped, not diagnosed.

## Documentation

- [CLI reference](https://github.com/AgiFlow/public-packages-internals/blob/main/packages/mcp/workflow-mcp/docs/cli.md): all commands, flags, and examples
- [Workflow YAML schema](https://github.com/AgiFlow/public-packages-internals/blob/main/packages/mcp/workflow-mcp/PROMPT.md): authoring reference for workflow files

## License

MIT
