Tutorial: Automating GitHub Issues
By the end of this tutorial you will have a Cloche pipeline that watches a GitHub repository for Issues, implements them autonomously inside a Docker container, and opens a pull request — all without manual intervention.
What We’re Building
The pipeline has four workflows. list-tasks queries GitHub for open issues. main orchestrates the full run from claiming the task through to the PR. Inside main, develop does the coding work in a container, and merge extracts those changes, rebases on main, and opens the PR.
Blue steps run on the host, orange runs inside a Docker container. We’ll zoom in on the develop and merge workflows as we get to them.
Prerequisites
Before starting, make sure you have:
- Cloche installed —
cloche --versionshould print a version number ghCLI installed and authenticated —gh auth statusshould show your account- Docker running —
docker infoshould succeed - A GitHub repository with a
Makefilethat has atesttarget (make testruns your test suite)
Initialise the Project
From the root of your repository, run:
cd my-project
cloche init --new
cloche init --new scaffolds a starter .cloche/ directory with workflow files, a Dockerfile, prompt templates, and scripts. It uses an LLM to fill in project-specific details such as the build command, test target, and coding conventions detected from your existing code. Without --new, cloche init only refreshes the .cloche/ config and registers the project with the daemon. See cloche init for the full flag table (--workflow, --base-image, --agent-command, --no-llm).
By the end of this tutorial your .cloche/ directory will look like this:
.cloche/
Dockerfile # Image used by container workflows
host.cloche # Host workflows (list-tasks, merge, main)
develop.cloche # Container workflow
prompts/ # Markdown prompt templates
design.md
implement.md
review.md
fix-review-feedback.md
fix.md
prepare-commit.md
fix-merge-conflicts.md
scripts/ # Shell helpers called by host steps
get-tasks.sh
create-worktree.sh
update-from-main.sh
create-review.sh
clean-up.sh
Run grep -r 'TODO(cloche-init)' .cloche/ to see any remaining placeholders that need manual attention.
Gathering Tasks
The list-tasks workflow is the entry point of the orchestration loop. Its job is to find work and print a JSONL list of tasks to stdout — one JSON object per line. Cloche captures that output and picks the first item with status: "open".
We use the gh CLI to fetch GitHub Issues tagged with the label cloche. Create .cloche/scripts/get-tasks.sh:
#!/bin/bash
set -e
gh issue list \
--label cloche \
--state open \
--json number,title,body \
--jq '.[] | {id: (.number | tostring), status: "open", title: .title, description: .body}'
Each output line is a JSON object. The id field is the issue number and becomes $CLOCHE_TASK_ID throughout the rest of the run. The description field is injected into agent prompts so the agent knows what to build.
Now add the workflow to .cloche/host.cloche:
workflow "list-tasks" {
host {}
step get-tasks {
run = "bash .cloche/scripts/get-tasks.sh"
results = [success, fail]
}
get-tasks:success -> done
get-tasks:fail -> abort
}
From list-tasks to main
list-tasks only decides what Cloche should work on. Execution happens in a second workflow named main — that is the contract with the orchestration loop. On each tick, Cloche runs list-tasks, picks an open task from its output, and triggers main with the task ID attached. These are the only two workflow names the orchestrator recognises.
Everything else — including the develop and merge workflows we’re about to write — is a sub-workflow that main invokes via workflow_name. In this tutorial, main will claim the task, call develop inside a container, then call merge on the host to open a pull request. We’ll build those pieces first, then wire them together in the final main workflow. See Orchestration Model for the full two-phase contract.
The Development Loop
The develop workflow runs inside a Docker container built from .cloche/Dockerfile. Your project files are available inside the container, and the agent has full access to them.
The workflow walks through a design–implement–review–test cycle. If review or tests find problems, the agent loops back and fixes them before moving on.
Purple nodes are agent steps; teal nodes are deterministic scripts. Red edges are failure wires; dashed orange edges are retry loops capped by max_attempts.
Create .cloche/develop.cloche with the following content:
workflow "develop" {
container {
image = "my-project:latest"
agent_command = "claude"
}
step design {
prompt = file(".cloche/prompts/design.md")
results = [success, fail]
}
step implement {
prompt = file(".cloche/prompts/implement.md")
results = [success, fail]
}
step review {
prompt = file(".cloche/prompts/review.md")
results = [success, fail]
}
step fix-review-feedback {
prompt = file(".cloche/prompts/fix-review-feedback.md")
max_attempts = 2
results = [success, fail, give-up]
}
step test {
run = "make test 2>&1"
results = [success, fail]
}
step fix {
prompt = file(".cloche/prompts/fix.md")
max_attempts = 2
results = [success, fail, give-up]
}
step prepare-commit {
prompt = file(".cloche/prompts/prepare-commit.md")
results = [success, fail]
}
step commit {
run = "bash .cloche/scripts/commit.sh"
results = [success, fail]
}
design:success -> implement
design:fail -> abort
implement:success -> review
implement:fail -> abort
review:success -> test
review:fail -> fix-review-feedback
fix-review-feedback:success -> review
fix-review-feedback:fail -> abort
fix-review-feedback:give-up -> abort
test:success -> prepare-commit
test:fail -> fix
fix:success -> test
fix:fail -> abort
fix:give-up -> abort
prepare-commit:success -> commit
prepare-commit:fail -> abort
commit:success -> done
commit:fail -> abort
}
A few things worth noting:
- Previous-step output in prompts. The
fixprompt template at.cloche/prompts/fix.mduses the{{ $prev_output }}template variable to reference the failing test output. Cloche substitutes it with the stdout of the immediately preceding step before invoking the agent, so the agent sees exactly what failed. max_attempts = 2onfixandfix-review-feedbackcaps the retry loop. On the final attempt, the step emitsgive-uprather thanfail, letting you wire a clean abort path.prepare-commitasks the agent to store the commit message and file list in the KV store usingclo set commit_msg "..."andclo set commit_files "file1 file2 ...". Thecommitstep reads them back withclo get. Since both steps share the same container, the KV store is the cleanest way to pass structured data between an agent step and a script step.
Create .cloche/scripts/commit.sh:
#!/bin/bash
set -e
MSG=$(clo get commit_msg)
clo get commit_files | xargs git add
git commit -m "$MSG"
Preparing the Pull Request
Once develop finishes, the container has a committed change on a local branch. The merge workflow extracts those changes into a git worktree on the host, rebases on main, and opens a PR.
fix-merge-conflicts is the only agent step in this workflow; the rest are shell scripts that shell out to git and gh.
Add this workflow to .cloche/host.cloche (the same file as list-tasks):
workflow "merge" {
host {}
step create-worktree {
run = "bash .cloche/scripts/create-worktree.sh"
results = [success, fail]
}
step update-from-main {
run = "bash .cloche/scripts/update-from-main.sh"
results = [success, fail]
}
step fix-merge-conflicts {
prompt = file(".cloche/prompts/fix-merge-conflicts.md")
results = [success, fail]
}
step create-review {
run = "bash .cloche/scripts/create-review.sh"
results = [success, fail]
}
step clean-up {
run = "bash .cloche/scripts/clean-up.sh"
results = [success, fail]
}
create-worktree:success -> update-from-main
create-worktree:fail -> abort
update-from-main:success -> create-review
update-from-main:fail -> fix-merge-conflicts
fix-merge-conflicts:success -> create-review
fix-merge-conflicts:fail -> abort
create-review:success -> clean-up
create-review:fail -> abort
clean-up:success -> done
clean-up:fail -> abort
}
Here are the scripts that back each step.
.cloche/scripts/create-worktree.sh — the container and host are isolated: the agent’s commits exist only inside the Docker container. This script creates a fresh branch and worktree on the host, copies the workspace out of the container with docker cp, and commits it — making the changes available to the remaining host-side steps:
#!/bin/bash
set -e
CHILD_RUN_ID=$(cloche get child_run_id)
BRANCH="cloche/${CHILD_RUN_ID}"
git worktree add ".cloche/worktrees/${CHILD_RUN_ID}" -b "$BRANCH"
docker cp "${CHILD_RUN_ID}:/workspace/." ".cloche/worktrees/${CHILD_RUN_ID}/"
cd ".cloche/worktrees/${CHILD_RUN_ID}"
git add -A
COMMIT_MSG=$(docker exec "${CHILD_RUN_ID}" git log -1 --format='%s')
git commit -m "$COMMIT_MSG"
Three IDs flow through a task:
CLOCHE_TASK_ID— the task identifier from your tracker (here, the GitHub issue number).CLOCHE_ATTEMPT_ID— a 4-character slug (e.g.a1f3) that disambiguates retries of the same task.CLOCHE_RUN_ID— identifies the current workflow run within an attempt.
None of these is the Docker container name. To reach the container produced by a sibling develop step, read the auto-seeded KV key child_run_id — the daemon writes it after a container sub-workflow completes, and its value matches the container’s name. The same value is also used as the suffix for the cloche/<run-id> branch convention, so the scripts below reuse it for both docker commands and the branch name. See Communicating Between Steps → Auto-seeded keys.
.cloche/scripts/update-from-main.sh — rebases the feature branch on the latest main. If this fails (conflicting changes), Cloche routes to the fix-merge-conflicts agent step:
#!/bin/bash
set -e
CHILD_RUN_ID=$(cloche get child_run_id)
cd ".cloche/worktrees/${CHILD_RUN_ID}"
git fetch origin main
git rebase origin/main
.cloche/prompts/fix-merge-conflicts.md — this is a prompt file you write yourself. It should instruct the agent to inspect the conflict markers, resolve them sensibly, and stage the result. No shell script needed; the agent handles it directly.
.cloche/scripts/create-review.sh — pushes the branch and opens the PR using gh. The PR body automatically references the originating issue:
#!/bin/bash
set -e
CHILD_RUN_ID=$(cloche get child_run_id)
BRANCH="cloche/${CHILD_RUN_ID}"
cd ".cloche/worktrees/${CHILD_RUN_ID}"
git push origin "$BRANCH"
gh pr create \
--title "$(git log -1 --format='%s')" \
--body "Automated by Cloche. Closes #${CLOCHE_TASK_ID}" \
--base main \
--head "$BRANCH"
.cloche/scripts/clean-up.sh — closes the GitHub Issue and removes the worktree. The branch itself is kept so the PR remains open for review:
#!/bin/bash
set -e
CHILD_RUN_ID=$(cloche get child_run_id)
gh issue close "${CLOCHE_TASK_ID}" --comment "Implemented in PR — closing."
git worktree remove ".cloche/worktrees/${CHILD_RUN_ID}"
Bringing It Together
The main workflow is the top-level orchestrator. It claims the task, calls develop, then calls merge. Add it to .cloche/host.cloche:
workflow "main" {
host {}
step claim-task {
run = "gh issue edit ${CLOCHE_TASK_ID} --add-label 'in-progress' --remove-label 'cloche'"
results = [success, fail]
}
step develop {
workflow_name = "develop"
results = [success, fail]
}
step merge {
workflow_name = "merge"
results = [success, fail]
}
claim-task:success -> develop
claim-task:fail -> abort
develop:success -> merge
develop:fail -> abort
merge:success -> done
merge:fail -> abort
}
The claim-task step swaps the cloche label for in-progress on the GitHub Issue. This ensures list-tasks won’t pick up the same issue again on the next loop tick while work is already in progress.
The develop and merge steps use workflow_name to invoke their respective workflows as sub-workflows. The container and host are fully isolated — merge is responsible for extracting the agent’s committed changes out of the container and into a host-side branch, which create-worktree.sh handles.
Running the Pipeline
First, validate that your workflow files parse without errors:
# Validate the workflow files first
cloche validate
# Check that everything is wired up correctly
cloche doctor
When both commands pass, start the orchestration loop. The --max 2 flag lets Cloche run up to two issues in parallel:
# Start the orchestration loop (up to 2 parallel runs)
cloche loop --max 2
Open the web dashboard at http://localhost:8080 to watch runs in real time. For terminal monitoring, use cloche status to see active runs and cloche logs <run-id> to tail the output of a specific run.
Label a GitHub Issue with cloche and watch the pipeline pick it up automatically within seconds.
Next Steps
- Customise the prompts — the templates in
.cloche/prompts/are the main lever for tuning quality. Add your project’s coding conventions, preferred libraries, and style rules. - Extend task filtering — add milestone filters, assignee checks, or priority labels to
get-tasks.shto control which issues Cloche picks up. - One-off runs — use
cloche run develop --prompt "Add dark mode support"to trigger a single develop run without going through GitHub Issues. - Explore the DSL — read the DSL Reference for advanced features like parallel branches and poll steps.