A harness for long-running automated loops using the Goose framework
Find a file
2026-06-23 13:14:25 -04:00
engines Polish docs and remove ADRs for public release 2026-06-23 12:57:50 -04:00
gooseloop Polish docs and remove ADRs for public release 2026-06-23 12:57:50 -04:00
tests Polish docs and remove ADRs for public release 2026-06-23 12:57:50 -04:00
.gitignore Initial commit: gooseloop execution shell 2026-06-13 15:05:13 -04:00
CLAUDE.md Polish docs and remove ADRs for public release 2026-06-23 12:57:50 -04:00
doc-map.example.toml Polish docs and remove ADRs for public release 2026-06-23 12:57:50 -04:00
gooseloop.toml Polish docs and remove ADRs for public release 2026-06-23 12:57:50 -04:00
LICENSE Initial commit: gooseloop execution shell 2026-06-13 15:05:13 -04:00
pyproject.toml Fix install URLs to the real gitforge repo path 2026-06-23 13:01:27 -04:00
README.md Explain why bundled engines need python -m, not the bare command 2026-06-23 13:14:25 -04:00

gooseloop

python license protocol

An execution shell for goose recipe pipelines.

Every loop runs the same three-phase shape:

review  ->  body  ->  summary

review always runs first and emits a structured plan. body does the work that plan routed. summary always runs last and renders the ledger. The framework owns that order and an engine cannot change it. You fill in the three phases. That sandwich is the whole mental model, and it is the entire extension surface.

gooseloop is generic. It knows nothing about your domain. A loop plugs in through two objects, an Engine and an Environment, plus some recipes. Around them the framework handles retry, rate-limit backoff, session bookkeeping, recipe overlay merging, and a typed session ledger, so an engine is just the verbs and nouns of one job.

Why

goose runs a recipe. gooseloop runs a loop of recipes with a spine: a review that decides what to do, a body that does it, and a summary that reports it honestly, including the parts that failed. The contract between those phases is written down (PROTOCOL.md), so a recipe author, an engine author, and an environment author can each work against the same fixed shape instead of re-inventing one per project.

Install

Not on PyPI yet. Install from source:

pip install git+https://gitforge.ca/Mathew/gooseloop.git

Or from a checkout:

git clone https://gitforge.ca/Mathew/gooseloop.git
cd gooseloop
pip install -e .

Requires Python 3.11+, the goose CLI on $PATH, and a YAML recipe directory.

Quickstart

from gooseloop import GooseLooper, Engine, Environment, Phase, Pipeline

class HelloEnvironment(Environment):
    def env_vars(self) -> dict[str, str]:
        return {"GREETING": "hello"}

class HelloEngine(Engine):
    name = "hello-world"

    def pipeline(self, ctx) -> Pipeline:
        return Pipeline(
            review=Phase(name="review", recipe_path="recipes/review.yaml"),
            body=[Phase(name="greet", recipe_path="recipes/greet.yaml")],
            summary=Phase(name="summary", recipe_path="recipes/summary.yaml"),
        )

GooseLooper(engine=HelloEngine(), environment=HelloEnvironment()).begin_loop()

A working version of this engine ships under engines/hello_world/.

The three primitives

  • Engine (the verbs). Returns a Pipeline(review, body, summary). Owns branching rules, ships its recipe defaults, sets retry and scoring policy. Subclass gooseloop.Engine.
  • Environment (the nouns). Declares what the engine can see. The ABC has one abstract method, env_vars(). Anything a recipe needs pasted in as text is a method the recipe calls via env_method:<name>. Subclass gooseloop.Environment, or a gooseloop.contrib.* mixin if one fits.
  • Pipeline (the bookend). The review -> body -> summary dataclass an engine returns from pipeline(). Review runs first and emits structured routing; body phases run in queue order; summary runs last over the final ledger.

The one decision that shapes a new loop is who decides what the body does:

  • Model-driven routing: the review looks at state and decides which body units to run. Copy hello_world.
  • Deterministic routing: the work is already known and the model's only job is to do each unit. The engine builds the body itself in pipeline(). Copy doc_drift.

Getting this axis right up front is the difference between a clean first loop and an awkward one.

Built-in engines

Three reference engines ship in engines/. They are a teaching set, read in this order: each one adds exactly one layer of capability. They live alongside the framework rather than inside the installed wheel, so run them from a checkout (the repo root, where engines/ is importable) and select one with -e <module>.

Engine Read it for Routing Run from the repo root
hello_world every contract in its simplest form model-driven python3 -m gooseloop run -e engines.hello_world
git_recap real I/O, content pasting, glob-read summary; recaps recent commits across repos into a changelog model-driven python3 -m gooseloop run -e engines.git_recap
doc_drift the advanced end: deterministic body, file bundles, live URL fetch, cross-run state; finds derived docs/pages that fell behind their source and drafts a patch engine-driven python3 -m gooseloop run -e engines.doc_drift

Drop -e to run whatever [gooseloop] engine_module points at (hello_world by default). Run the bundled engines with python3 -m gooseloop: they live in ./engines, and a console script (unlike python -m) does not put the current directory on sys.path, so the bare gooseloop command cannot import them (PYTHONPATH=. gooseloop ... works too). Your own pip-installed engine needs no such trick: plain gooseloop finds it. Add --review-only to stop after the review phase.

Once your project has its own engine, the examples can be deleted wholesale. The framework does not depend on any of them.

Recipe overlay merge

Recipes compose docker-compose style. The looper resolves layers in order, later winning:

  1. Base recipe (declared in gooseloop.toml).
  2. <name>.local.yaml (gitignored; per-machine).
  3. --review-overlay X.yaml / --summary-overlay X.yaml (ad-hoc).

Inspect the merged result with gooseloop recipe --resolve <name>. The full merge-rules table is PROTOCOL.md §6.

Contrib mixins

Domain-shaped Environment ABCs ship under gooseloop.contrib, for loops whose shape recurs:

  • CustomerPipelineEnvironment: customer-acquisition pipelines (build_digest(), journal_text(), lifecycle_dirs(), ...).
  • ClaudeHandoffEnvironment: design-handoff engines (handoff_dir(), target_repo(), dev_up_probe(), ...).

An engine with no fit subclasses bare Environment and writes one method.

CLI

gooseloop run                      # run the configured engine, one pass
gooseloop run --review-only        # stop after review
gooseloop run --review-overlay x.yaml --summary-overlay y.yaml
gooseloop recipe --resolve review  # print the fully-merged recipe
gooseloop engines                  # list known engines from gooseloop.toml

gooseloop.toml at the project root configures which engine, which recipes, retry tuning, and the sessions directory.

Documentation

  • gooseloop/PROTOCOL.md is canonical: the review JSON schema, the summary contract, body-phase rules, BranchPolicy, recipe overlay merge, and the recipe context: block. If the code disagrees with it, the code is the bug.
  • CLAUDE.md is the entry point for an AI agent authoring a loop.

License

MIT. See LICENSE.