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.

flowchart LR LT[list-tasks]:::host -->|open issue| M[main]:::host M --> CT[claim-task]:::host CT --> DEV[develop]:::container DEV --> MRG[merge]:::host MRG --> DONE((done)):::terminal classDef host fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef container fill:#fff3e0,stroke:#ef6c00,color:#bf360c classDef terminal fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20

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 installedcloche --version should print a version number
  • gh CLI installed and authenticatedgh auth status should show your account
  • Docker runningdocker info should succeed
  • A GitHub repository with a Makefile that has a test target (make test runs 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
Check for placeholders

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.

flowchart TD design --> implement implement --> review review -->|success| test review -->|fail| fixreview[fix-review-feedback] fixreview -->|success| review test -->|success| prepare-commit test -->|fail| fix fix -->|success| test prepare-commit --> commit commit --> DONE((done)) classDef agent fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c classDef script fill:#e0f2f1,stroke:#00695c,color:#004d40 classDef terminal fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20 class design,implement,review,fixreview,fix,prepare-commit agent class test,commit script class DONE terminal linkStyle 3,6 stroke:#c62828,stroke-width:2px linkStyle 4,7 stroke:#ef6c00,stroke-width:2px,stroke-dasharray:4 3

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 fix prompt template at .cloche/prompts/fix.md uses 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 = 2 on fix and fix-review-feedback caps the retry loop. On the final attempt, the step emits give-up rather than fail, letting you wire a clean abort path.
  • prepare-commit asks the agent to store the commit message and file list in the KV store using clo set commit_msg "..." and clo set commit_files "file1 file2 ...". The commit step reads them back with clo 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.

flowchart TD create-worktree --> update-from-main update-from-main -->|success| create-review update-from-main -->|fail| fixconflicts[fix-merge-conflicts] fixconflicts --> create-review create-review --> clean-up clean-up --> DONE((done)) classDef script fill:#e0f2f1,stroke:#00695c,color:#004d40 classDef agent fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c classDef terminal fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20 class create-worktree,update-from-main,create-review,clean-up script class fixconflicts agent class DONE terminal linkStyle 2 stroke:#c62828,stroke-width:2px linkStyle 3 stroke:#ef6c00,stroke-width:2px,stroke-dasharray:4 3

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"
Cloche run IDs and the container name

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.

Try it now

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.sh to 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.