The SWE Process (GitLab)

The SWE process is Pinard’s reference loop — a semi-deterministic loop that turns a GitLab issue into a merged merge request: issue → change → MR → review → merge → reap. It is a complete, batteries-included application built on the engine — and a template for loops of your own.

This is one process, not the whole of Pinard. If your fleet does something other than code review, you write a different babysitter process; everything below is how this particular loop is wired.

Setup — the Pinard GitLab account

The SWE loop acts through a dedicated GitLab service account (role: Developer).

ActionAPIRole
Push branchesSSHDeveloper
Open / merge / comment on MRs/merge_requests, /notesDeveloper
Create issues, read pipelines/issues, /pipelinesDeveloper
  • Personal access token — scopes api, read_repository, write_repository.
  • SSH keyssh-keygen -t ed25519 -C "pinard" -f ~/.ssh/pinard_id_ed25519 -N "", then add the public key to the account.
  • Branch protection — the default branch should allow “Developers + Maintainers” to merge, otherwise Pinard can’t auto-merge.

Trigger — assign an issue

The daemon’s issue watcher scans every vigne for open issues assigned to the Pinard user. Assignment is the request — there is no opt-in label and no confirmation step. When it finds one, it spawns a vendangeur with the issue as context and labels the issue in-progress.

Owner gate (security)

Pinard only spawns vendangeurs for work the vignoble owner has authorized. This prevents any GitLab user with push access from spending your LLM quota by assigning issues to the Pinard bot.

An issue passes the gate when either is true:

  • The issue author is the owner (you created it yourself).
  • The owner explicitly approves: leave a comment on the issue @-mentioning the Pinard user with an approval keyword (approve, approved, or go), or assign the Pinard user to the issue yourself (a system assignment note authored by the owner counts as approval).

When an issue is assigned to Pinard but does not yet pass the gate, the watcher:

  1. Labels the issue pinard:awaiting-approval.
  2. Posts a comment on the issue explaining what is needed.
  3. Polls each cycle — as soon as the owner approves, the vendangeur spawns.

The gate is fail-closed: if owner is not configured in credentials.yaml, no auto-spawning happens at all.

Configuring the gate

Set owner in credentials.yaml to the GitLab username of the vignoble operator (typically your own account):

nats:
  user: lelongs   # this becomes the owner automatically

The nats.user field is used as the owner by default. If you need to specify a different owner (e.g. when the NATS user and GitLab owner differ), override it with owner: in the credentials file.

To allow the conductor’s spawn_agent tool to assign issues as the owner (so that assignment itself counts as approval), also configure owner_token_env:

gitlab:
  owner_token_env: PINARD_OWNER_GITLAB_TOKEN   # human operator's PAT

When PINARD_OWNER_GITLAB_TOKEN is set, the conductor uses it for issue assignment (the system note is then authored by you, not the bot), and Pinard can auto-approve its own spawns without requiring a separate approval comment.

Security note. The owner token is only emitted for the conductor role (aoc env-exports --role conductor). Workers receive the bot token only and can never hold the operator’s GitLab PAT — even if it is in the daemon’s environment, aoc spawn passes workers an explicit, allowlisted environment.

Labels that gate spawning

LabelEffect
blockedSkipped — no vendangeur spawned
pinard:discardedSkipped; if already spawned, its state resets so it can retry
pinard:awaiting-approvalHeld — owner hasn’t approved yet; watcher re-checks each cycle. Removed automatically when the owner approves.
capsule:awaiting-fundingCapsule-gated — contract detected but not yet funded; see Buddy Capsules

Retry: add pinard:discarded, then remove it and re-assign — the watcher picks it up next cycle.

Labels that route the work

LabelEffect
parcelle:<name>Route into a parcelle (else the vigne’s own bucket)
target:<branch>Target a branch, e.g. target:cuvee/data-service (may contain /)

A parcelle can also claim an issue via its parcelle.yaml, which may set a target_branch: (the cuvée strategy) without any label.

The loop — issue to MR

A sketched software-work lifecycle from an approved request through an isolated worker and merge-request watcher, with review and failure loops, to merge and worker cleanup. Owner gate Vendangeur MR handoff MR watcher Review feedback Pipeline retry Circuit breaker Merge · post-merge · reap
Issue to merge, with one deterministic caretaker. The watcher routes review and pipeline failures back to the vendangeur, and owns the successful path through merge and cleanup.
  • Owner gate — assignment becomes work only after owner authorization.
  • Vendangeur → MR handoff — the isolated worker implements and validates, then track_mr transfers care.
  • MR watcher — reviews and failed pipelines return to the worker; repeated failure reaches the circuit breaker.
  • Merge · post-merge · reap — approval and green pipelines lead to merge, final monitoring, and deterministic cleanup.
StageWhat happens
DetectedWatcher publishes issues_new
SpawnedVendangeur created in its own git worktree, issue as context
In progressIssue labelled in-progress
WorkingThe loop makes the change, validates it, and opens an MR
TrackedThe worker calls track_mr(mr: N) so the MR watcher takes over

Comments on a tracked issue are forwarded to the conductor as issues_comment events (Pinard’s own comments are filtered, so there’s no feedback loop).

The MR watcher

Once an MR is tracked (written to .state/mr-watcher.yaml, polled every ~30s), the daemon tends it through its whole lifecycle:

Worker opens MR → track_mr → MR watcher
    ├── Review comments → forwarded to the worker (threaded via discussion_id)
    ├── Pipeline fails   → dispatched to the worker (attempt X/5)
    ├── Pipeline passes  → informational event to the conductor
    ├── Approved + green  → auto-merge (only if enabled — off by default)
    ├── Merged (human or auto) → post-merge pipeline + tag monitoring
    ├── Circuit breaker (5 failures) → worker killed
    └── Terminal condition → reapWorker (the single teardown point)

Review forwarding

New reviewer notes are published as review_comment events carrying discussion_id (for threaded replies) and file/line (for inline comments), and dispatched straight to the worker’s inbox — including the exact glab api …/discussions/<id>/notes command to reply in-thread.

Auto-merge (optional)

Off by default — a human merges. When enabled via auto_merge: true in vignes.yaml (per-vigne or global), the watcher merges once all hold: pipeline success, at least one approval, no unresolved threads, and not a Draft. If unapproved, a needs_approval event goes to the conductor. With auto-merge off, none of this runs.

Post-merge & reap

After merge, the watcher monitors the main and tag pipelines (reporting to the conductor), and finally calls reapWorker — the single, deterministic teardown point that kills the tmux session and cleans up the worktree.

See also