Architecture

Puppets is a cloud-only harness: each managed repository initiates its own run, grants its own permissions, and calls shared public code using GitHub Actions compute. There is no inbound controller, external state store, or broad cross-repository token.

Puppets repository ownership and control flow The managed repository owns its caller workflow, policy, secrets, issues, pull requests, and checks. The public Puppets repository owns the reusable workflow, runtime, defaults, and prompts. GitHub Actions runs that shared code in the managed repository context and sends approved implementation work to Copilot, Claude Code, or Codex. YOUR REPOSITORY Managed repository Caller workflow .github/workflows/puppets.yml Repository policy .puppets/config.json workflow.yml + prompts/ GitHub state and output Issues + approval labels Repository secrets Implementation branches Draft pull requests CI checks + review state PUBLIC REPOSITORY JeffSteinbok/puppets Reusable workflow .github/workflows/reconcile.yml Shared framework runtime + defaults + prompts EPHEMERAL COMPUTE GitHub Actions run Runs with your repository's token and permissions 1 Load policy 2 Advance work 3 Check result Reads state, takes bounded safe steps, writes the result back SELECTED PER PROFILE Implementation provider GitHub Copilot Claude Code OpenAI Codex 1 2 3 4 5
  1. The caller invokes the shared workflow.
  2. Actions loads framework code and trusted local policy.
  3. Puppets sends approved work to the selected provider.
  4. The provider's changes return as a branch and draft PR.
  5. CI and review results feed the next reconciliation pass.

Responsibility split

Concern Managed repository Public framework
Schedule and event triggers Owns Receives the invocation
Permissions Declares the maximum Cannot elevate them
Repository mutations Uses local token Executes through caller context
Lifecycle defaults May safely overlay Owns canonical model
Prompts May replace trusted files Owns canonical prompts
Runtime code None copied locally Versioned and tested centrally
Upgrade Changes one framework version Publishes compatible versions

What happens during a run

  1. Resolve the framework revision. Puppets reads GitHub's signed reusable-workflow claim to identify the selected framework version.
  2. Check out trusted sources separately. The framework and caller default branch are placed in isolated directories with persisted Git credentials disabled.
  3. Resolve policy before mutation. The versioned basic workflow merges with the caller's workflow overlay and prompts, then compiles into a validated state machine.
  4. Reconcile labels and state. The runtime derives current state from GitHub, verifies approval provenance, and takes all immediately allowed steps (bounded per run) until no further safe transition is available.
  5. Publish an auditable summary. Outputs and the workflow summary report assignments, waiting items, and the resolved policy sources.

Framework packaging

The caller selects a published framework version in its uses reference. Puppets uses GitHub’s signed reusable-workflow identity to load the matching framework runtime.

The reusable workflow never assumes framework files exist in the caller checkout. It checks out the public framework into an isolated directory, installs the locked runtime, and loads the reconciler from that revision.

jobs:
  reconcile:
    uses: JeffSteinbok/puppets/.github/workflows/reconcile.yml@v1
    with:
      dry_run: $

Trigger strategy

Version 1 uses a daily schedule and manual dry runs. Repository-local event triggers may be added later, while the schedule remains the self-healing reconciliation pass.

Schedules should be staggered across repositories to spread API and model traffic. Event triggers, when added, remain local and invoke the same reusable workflow—never an inbound webhook or cross-repository dispatch.

State ownership

The issue is the authoritative state record. Pull-request labels are projections and cannot advance the linked issue.

This makes reconciliation repeatable: every run derives state from labels and managed comments rather than trusting an external database or a previous runner process. Overlapping runs are serialized by the caller’s repository-scoped concurrency group.

Implementation providers

A profile’s implementation.provider (copilot by default, or claude/codex) selects who performs the implementation step. Copilot is assigned directly; Claude and Codex run in the provider job shown above and hand their workspace changes back to Puppets, which opens a draft pull request. The rest of the lifecycle remains provider-agnostic and inspects the resulting pull request.

See Configuration → Implementation providers for setup.

State machine

The primary lifecycle is intentionally linear. A profile can skip curation and claim an approved issue directly; issues needing more detail wait before approval.

stateDiagram-v2
    direction LR
    state "needs-info" as needs_info
    state "in-review" as in_review

    [*] --> untracked
    untracked --> needs_info: needs-info
    untracked --> approved: approved label
    needs_info --> approved: approved label
    approved --> curating: curate
    curating --> ready: pass
    ready --> claimed: claim
    approved --> claimed: direct claim
    claimed --> verifying: verify
    verifying --> in_review: pass
    in_review --> done: complete
    done --> [*]

Recovery paths are shown separately so they do not obscure the main flow. Retry outcomes remain in the current stage; actionable failures enter needs-work; policy decisions or exhausted retries enter needs-human.

stateDiagram-v2
    direction LR
    state "active stage" as active
    state "needs-work" as needs_work
    state "needs-human" as needs_human

    active --> active: retry or wait
    active --> needs_work: remediate
    needs_work --> needs_work: retry
    needs_work --> active: verify again
    active --> needs_human: escalate
    needs_work --> needs_human: escalate
    needs_human --> active: approve or queue
    active --> done: complete
    needs_human --> done: complete

Branch names are declared outcomes, not model choices: trusted handlers emit an outcome and the compiled machine selects its target.

Trust boundary

The runtime, not editable workflow data, enforces approval provenance, current actor permission, the configured opt-out role, trusted-default-branch configuration, fork isolation, and transition validation. Issue, pull-request, diff, and check text remains untrusted data, and provider selection cannot replace framework-controlled actions or weaken these invariants.