pipeline-worker
v0.1.29
Published
Automated git-worktree workflow: captures intent from a diff via Claude Code or GitHub Copilot CLI, runs build/lint/test, opens a GitLab MR or GitHub PR, and auto-fixes failing pipelines — with a companion forge MCP server (TOON-encoded responses).
Maintainers
Readme
pipeline-worker
Automate the last mile of your local changes: pipeline-worker takes the uncommitted diff in your repo and drives it — unattended — all the way to a merged, locally-synced result.
- Captures your staged + unstaged changes (your working tree is only read, not modified, up through this point).
- Replays them in a disposable git worktree.
- Asks a coding agent (Claude Code, Pi, or GitHub Copilot CLI) to infer the intent: change type, branch slug, commit message, summary.
- Runs your
build/lint/testcommands, fail-fast. - Commits, pushes, and opens a GitLab MR or GitHub PR — the branch name is composed from the configurable
branchPattern. - Polls the CI pipeline; on failure it hands the pipeline URL to the agent, which pulls the failed jobs and logs itself via whatever GitLab/GitHub MCP tooling is available (pipeline-worker's own forge MCP server, or an external one the agent already has configured), commits the fix, pushes, and re-polls — capped at
maxFixAttemptsbefore escalating to a human with an MR comment. - Once the MR/PR is ready to merge (or, with
PIPELINE_WORKER_CLEANUP_EARLY, as soon as the MR/PR is opened), resets your repo's current branch back to HEAD (seePIPELINE_WORKER_CLEANUPbelow) — your changes now live safely on the feature branch instead of sitting uncommitted locally too. - By default (
PIPELINE_WORKER_AUTO_MERGE_ON_GREEN), waits for the forge to confirm the auto-merge actually landed, then (after a few seconds' grace for the ref to settle) fast-forwards your local target branch from origin — so your local main already contains the merged result when the run ends. Best-effort: if the merge is held up (e.g. by required approvals), you switched branches mid-run, or your local target branch diverged, it leaves everything untouched and tells you togit pullinstead. SetPIPELINE_WORKER_AUTO_MERGE_ON_GREEN=falseto go back to opening the MR/PR and merging it yourself.
While attached to a real terminal, the run renders as a live step tree — header line, then one row per step (capture, worktree, checks, ci-watch, merge, ...) with a status glyph, duration, and best-effort token count, updated in place. CI logs and piped output fall back to the previous append-only narration (or force it yourself with PIPELINE_WORKER_PLAIN_OUTPUT=true).
Polling is plain REST and costs zero agent tokens; the agent is invoked only when a pipeline actually fails, and fetches whatever pipeline/job detail it needs through pipeline-worker's token-efficient TOON-encoded MCP server (or an external forge MCP server, if the agent has one available).
Requirements
- Node.js >= 20.12 and git
- One coding agent CLI on your PATH: Claude Code (
claude), Pi (pi), or GitHub Copilot CLI (copilot) - A GitLab or GitHub token with API access to the repo
Agents
| CLI | PIPELINE_WORKER_AGENT | Setup | Per-invocation model selection |
| --- | ----------------------- | ----- | ------------------------------ |
| Claude Code | claude | npm install -g @anthropic-ai/claude-code | ✅ (--model) |
| Pi | pi | npm install -g @earendil-works/pi-coding-agent | ✅ (--model) — any provider/model |
| GitHub Copilot CLI | copilot | Install via GitHub's docs | ❌ (uses its own configured model) |
Pi supports models from any provider — Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, OpenRouter, etc.
Configure your provider/api-key via pi's own setup (/login), env vars, or --provider in the adapter.
Install
npm install -g pipeline-workerThis installs two equivalent commands, pipeline-worker and the shorter pw — use whichever you prefer (e.g. pw run --ticket PROJ-123).
Quick start
Set these once in your shell profile (~/.zshrc / ~/.bashrc) and every
repo on the machine picks them up — no per-repo setup needed:
export PIPELINE_WORKER_AGENT=claude # or pi, or copilot
export PIPELINE_WORKER_FORGE=gitlab
export PIPELINE_WORKER_GITLAB_HOST=https://gitlab.example.com
export PIPELINE_WORKER_GITLAB_TOKEN=glpat-xxxxx
export PIPELINE_WORKER_GITLAB_REPO_BASE=$HOME/REPO # local dir that mirrors the GitLab namespace root — enables auto-detected projectId in any repo underneath itThen, in any repo:
cd your-repo
# hack, hack, hack — leave the changes uncommitted, then:
pipeline-workerConfiguration
pipeline-worker is configured entirely through real environment variables — set them in your shell profile once, and every repo picks them up.
| Env var | Default | Meaning |
| --------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------- |
| PIPELINE_WORKER_AGENT | claude | claude, pi, or copilot |
| PIPELINE_WORKER_FORGE | gitlab | gitlab or github |
| PIPELINE_WORKER_GITLAB_HOST | — | e.g. https://gitlab.example.com |
| PIPELINE_WORKER_GITLAB_PROJECT_ID | — | numeric project id |
| PIPELINE_WORKER_GITLAB_REPO_BASE | — | local dir mirroring the GitLab namespace root, for auto-detecting projectId |
| PIPELINE_WORKER_GITLAB_TOKEN | — | GitLab API token |
| PIPELINE_WORKER_GITHUB_REPO | auto-detected from origin | owner/name slug — only needed when origin isn't a GitHub remote |
| PIPELINE_WORKER_GITHUB_TOKEN | falls back to GITHUB_TOKEN | GitHub token |
| PIPELINE_WORKER_POLL_INTERVAL_SECONDS | 15 | pipeline poll cadence; use 60 for slow pipelines |
| PIPELINE_WORKER_BRANCH_PATTERN | pipeline-worker/{name} | feature branch naming template — see below |
| PIPELINE_WORKER_CLEANUP | true | reset repoRoot to HEAD once cleanup fires (see PIPELINE_WORKER_CLEANUP_EARLY for when) (false to keep your local uncommitted changes as-is) |
| PIPELINE_WORKER_CLEANUP_EARLY | false | true resets repoRoot as soon as the MR/PR is opened (diff committed + pushed), instead of waiting for CI to go green — frees the repo (and the run lock) for a new pipeline-worker run while this run's CI-watch/fix loop keeps going in the background |
| PIPELINE_WORKER_INTENT_MODEL | haiku | model used for the intent-capture step (branch/commit/summary); claude and pi support per-invocation model selection — copilot ignores it |
| PIPELINE_WORKER_BUILD | auto-detected from toolchain | build command override; set to an empty string to skip the stage |
| PIPELINE_WORKER_LINT | auto-detected from toolchain | lint command override; set to an empty string to skip the stage |
| PIPELINE_WORKER_TEST | auto-detected from toolchain | test command override; set to an empty string to skip the stage |
| PIPELINE_WORKER_MAX_FIX_ATTEMPTS | 5 | how many CI-fix attempts before escalating to a human — tracked independently from merge-conflict-resolution attempts, so a long-lived PR needing several rebases can't exhaust the budget meant for real bug-fixing |
| PIPELINE_WORKER_RUN_LINT_AND_TEST | true | run the local lint and test stages (false to run only build — for repos where an earlier workflow, e.g. upstream CI, already verified lint/test) |
| PIPELINE_WORKER_UPDATE_CHANGELOG | false | once checks pass, add a bullet (from the captured intent's summary) under CHANGELOG.md's [Unreleased] section — feature/bugfix/chore map to the Added/Fixed/Changed categories — creating the file, Keep a Changelog-style, if the repo has none — and include it in the same commit |
| PIPELINE_WORKER_AUTO_MERGE_ON_GREEN | true | once the MR/PR opens, ask the forge to merge it automatically as soon as CI (and any required approvals) allow — best-effort; if the forge rejects it (auto-merge not enabled for the repo, pending approvals, ...) the run continues normally and you merge manually. Once the forge confirms the merge landed, the run also fast-forwards your local target branch from origin (waiting a few seconds for the ref to settle first), so your local main is already up to date when the run ends. Set to false to go back to opening the MR/PR and merging it yourself |
| PIPELINE_WORKER_MERGE_METHOD | squash | merge, squash, or rebase — passed to auto-merge. GitLab has no per-request rebase option; rebase there falls back to the project's own default merge method |
| PIPELINE_WORKER_SQUASH_ON_MERGE | false | once CI is green, collapse every commit this run made on the branch into one (titled from the captured intent) and force-push — keeps history clean regardless of the repo's merge-strategy setting. Off by default: rewrites published history (force-push), a materially different risk from everything else this tool does. Only reliable with auto-merge off — the forge may already have merged (and deleted) the branch before this step runs |
| PIPELINE_WORKER_PLAIN_OUTPUT | false | force the append-only, non-redrawing narration even on a real terminal (the same output CI/piped runs always get) — useful when pasting output into a bug report or feeding it to another tool |
Branch naming
PIPELINE_WORKER_BRANCH_PATTERN controls the feature branch name, built from three placeholders:
| Placeholder | Filled by |
| ----------- | ------------------------------------------------------------------- |
| {type} | feature, bugfix, or chore — inferred from the diff by the agent |
| {ticket} | the --ticket <id> flag passed to pipeline-worker run |
| {name} | a short kebab-case slug describing the change — inferred by the agent |
For example, a team using GitLab issue-linked branches would set:
export PIPELINE_WORKER_BRANCH_PATTERN='{type}/{ticket}/{name}'pipeline-worker run --ticket PROJ-123
# -> bugfix/PROJ-123/fix-login-redirectA pattern that includes {ticket} requires --ticket to be passed; the run fails fast at the naming step otherwise.
Check command auto-detection
build / lint / test are picked from the repo's toolchain (first marker found wins; mixed-language repos should set PIPELINE_WORKER_BUILD / PIPELINE_WORKER_LINT / PIPELINE_WORKER_TEST explicitly):
| Toolchain | Marker | build | lint | test |
| ----------------- | ------------------------------------------------------ | ---------------- | ----------------------------------- | ------------------------------------------------ |
| Node / TypeScript | package.json | npm run build | npm run lint | npm test — each only if the script is declared |
| .NET | *.sln / *.csproj / *.fsproj / *.vbproj at root | dotnet build | dotnet format --verify-no-changes | dotnet test |
| Go | go.mod | go build ./... | go vet ./... | go test ./... |
| Python | pyproject.toml / setup.py / requirements.txt | — | — | pytest |
A stage with no command (—) is skipped. If no toolchain is detected and no commands are configured, all local checks are skipped with a warning.
Commands
| Command | What it does |
| -------------------------------------------- | ------------------------------------------------------------------------- |
| pipeline-worker (or pipeline-worker run) [--ticket <id>] | Capture the current diff and drive it to a green MR/PR |
| pipeline-worker serve | Start the forge MCP server over stdio (used by the agent during fix runs) |
| pipeline-worker resume --branch <name> [--target <branch>] | Resume watching/fixing a run after a crash, or adopt a branch pipeline-worker has no record of |
| pipeline-worker status --branch <name> | Print the persisted state of a run |
| pipeline-worker sessions [--branch <name>] | List every persisted run in this repo, or show one run's full step-by-step timeline |
| pipeline-worker update | Install the latest release from npm (npm install -g pipeline-worker@latest) |
Before doing any work, pipeline-worker run checks npm for a newer published version and installs it automatically if the locally installed one is out of date (the update takes effect on the next run). This check is best-effort: if npm is unreachable or the install fails, the run proceeds anyway on whatever version is already installed.
Adopting a branch pipeline-worker never ran on
pipeline-worker resume --branch <name> also works for a branch pipeline-worker has no persisted state for at all — e.g. one you committed and pushed by hand. It checks out the branch and checks the forge for an open PR/MR for it:
- No PR/MR yet: runs it like a fresh
pipeline-worker runfrom this point on — build/lint/test checks (aborting the same way a normal run does on failure), intent capture, then opens the MR/PR — targeting--target <branch>if given, or origin's auto-detected default branch otherwise. - PR/MR already open: re-captures intent from the branch's actual diff, overwrites the PR/MR's description with it (using the PR/MR's own target branch — no guessing needed), and resumes the normal watch/fix loop: poll CI, and on failure pull the failed jobs' logs, hand them to the agent to fix, commit, push, and repoll.
Every time a run hands a turn to the agent (resolving a conflict, capturing intent, fixing a failed pipeline), the output includes that turn's duration and an agent session: <id> line — claude --resume <id>, pi --session <id>, or copilot --resume <id> opens the same session later to see exactly what it did and why. Copilot CLI has no way to report the session id it picked for itself, so pipeline-worker assigns one via --name instead and reports that.
How the fix loop stays bounded
Every retry path has a cap: local checks abort the run before an MR is ever opened; if no CI pipeline shows up for the MR/PR within 60s, the run ends there instead of polling; otherwise pipeline polling gives up after a 2-hour safety window; fix attempts stop at maxFixAttempts; a fix attempt that changes no files, or a pipeline that ends canceled/skipped, escalates immediately instead of spending agent tokens. Escalation always leaves a comment on the MR/PR so a human knows to take over.
