No description
  • TypeScript 100%
Find a file
2026-06-24 00:09:35 +02:00
.githooks build: enforce Biome via npm scripts and pre-commit hook 2026-06-21 20:26:31 +02:00
docs docs: note pippero→pier rename in historical design docs 2026-06-23 22:21:03 +02:00
src refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
test refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
.gitignore chore: update prompt 2026-06-21 21:34:06 +02:00
biome.json fix(biome): use non-deprecated preset key for recommended rules 2026-06-21 20:01:51 +02:00
config.example.yaml refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
CONTEXT.md refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
package-lock.json refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
package.json refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
PRD.md refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
README.md refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
slack-app-manifest.json refactor: rename pippero to pier across code, config, and user docs 2026-06-23 22:15:43 +02:00
tsconfig.json chore: scaffold t2p TypeScript project with vitest harness 2026-06-18 16:14:01 +02:00
vitest.config.ts chore: scaffold t2p TypeScript project with vitest harness 2026-06-18 16:14:01 +02:00

pier — Self-Hosted Ticket-to-PR Agentic Pipeline

pier drives software issues from idea to merged change through chat, with a human gate before any autonomous code is written. It is the orchestrator: it never embeds a model client, it drives a separate general-purpose coding agent (PI) as a subprocess and performs every tracker action itself.

Status: code-complete (v0.1.0). All automated behaviour is implemented and tested (217 tests). The live end-to-end path is operator-validated — see Validation.

New here? Start with the Quickstart — a linear zero-to-first-PR walkthrough (install PI → configure → validate → groom → autonomous PR).

Two decoupled capabilities

pier comprises two fully-decoupled capabilities whose only handoff is the issue and its state labels:

  1. Grooming (interactive) — a Slack-threaded conversation refines a rough brief into an agreed plan, written into a new issue marked planned. Ends when a human runs /approve in the thread.
  2. Autonomous implementation — once a human marks an issue ready-to-dev, pier implements it in isolation, opens a PR, responds to review feedback, and merges on human approval.

Either can be used without the other; the handoff is just the issue and its label.

How it works

State lives on the issue/PR as a single label — applying one removes any other. ready-to-dev is the only human-applied state; all others are system-managed.

            grooming (chat only)
                  │ human: /approve
                  ▼
   ┌──────────► planned
   │              │ human applies: ready-to-dev   ◄── the gate
   │              ▼
   │           building ──────────────┐ failure / orphaned-on-restart
   │              │ build success     ▼
   │              ▼              needs-attention  (human re-applies ready-to-dev to retry)
   │   PR #M opened, issue label cleared
   │              │
   │          in-review ◄──┐ pier pushes fixes
   │              │        │ in place
   │   human review comment┘
   │              │ human approving review
   │              ▼
   └────────────done   →  PR merged (squash), `Closes #N` closes the issue, worktree cleaned up

The PR is the sole workflow item. On build success the originating issue's state label is cleared and a separate PR (#M) carries in-reviewdone; the issue is closed by Closes #N when the PR merges. Review feedback is addressed in place on the same branch and the PR stays in-review.

State labels (per repo): planned, ready-to-dev, building, in-review, addressing-feedback, done, needs-attention.

Control model

A level-triggered reconcile engine (Kubernetes-controller style) is the spine. Every signal — webhook, poll tick, or startup sweep — is just a trigger to reconcile an item; reconcile(item) re-reads the item's current tracker state and computes the single correct next action. It never trusts an event payload as truth.

  • Idempotent — duplicate signals compute the same state → one action.
  • Restart-safe — a startup sweep reconstructs all in-flight work from labels alone; pier holds no critical workflow state. An item found building with no live process on restart moves to needs-attention.
  • The state label is the durable claim and the queue — waiting items sit at ready-to-dev and are re-enqueued by the sweep; bounded concurrency (default 1) with a per-item lock serialises triggers for the same item.

Architecture — ports and adapters

The orchestration core depends only on a few interfaces (ports); adapters implement them and the core never references a concrete service. This boundary is also the testing seam.

Port Responsibility First adapter
TrackerProvider labels, issues, PRs, merge, comments, event source Forgejo (Gitea REST) · GitHub (per-project)
ChatProvider thread messages, post, channel↔repo mapping Slack (Socket Mode — outbound only)
CodingAgent groom / build / addressFeedback PI (subprocess, --mode json)
ProcessRunner, FileIO shell + filesystem Node adapters

Every adapter has a Fake (src/testing/) so the full pipeline is exercised in-memory without Forgejo, Slack, PI, or Docker.

Security boundary (C5)

  • The coding agent holds only a repo-scoped push key (ephemeral 0600 temp file, injected only for the git push step, redacted from logs and comments). It can create a worktree, commit, and push its branch — nothing else.
  • pier performs every tracker action — labels, PR-open, merge, comments — using a single least-privilege Forgejo token the agent never sees.
  • Builds run inside the repo's devcontainer: the worktree is the only writable mount, egress restricted to the model backend and the git remote.
  • A redaction layer fronts the logger and the comment-poster; secrets are never logged.

Requirements

  • Node ≥ 22
  • PI with the superpowers skills installed (grill-me, brainstorming, writing-plans, subagent-driven-development, using-git-worktrees) and a models.json
  • A Slack app in Socket Mode (app + bot tokens)
  • A self-hosted Forgejo instance or a github.com repository, with an API token and a repo-scoped deploy key
  • Docker (for devcontainer build isolation)
  • A Tailscale tailnet between pier and Forgejo (no inbound exposure)

Configuration

Config is YAML, validated by Zod, fail-fast. Copy config.example.yaml and edit it. Secrets are referenced by environment-variable name — the values never live in the file, and are resolved from .env at startup. Reuse a credential across projects by naming the same env var in each.

Each project carries its own Forgejo connection:

global:
  defaultModelBackend: local/llama   # resolved by PI's models.json; use the provider/model form (a bare name can fuzzy-match a keyless provider)
  maxConcurrentBuilds: 1
  inactivityTimeoutMs: 1800000       # 30m watchdog (primary)
  absoluteCeilingMs: 21600000        # 6h backstop
  groomingRoot: /Users/you/pier/grooming
  sweepIntervalMs: 60000             # autonomous sweep cadence
  worktreeRoot: /Users/you/pier/worktrees
  logDir: /Users/you/pier/logs
projects:
  - repoUrl: https://forgejo.your-tailnet.ts.net/you/your-repo  # server + owner/name in one URL
    tokenEnv: PIER_FORGEJO_TOKEN  # NAME of the env var holding this repo's Forgejo token
    pushKeyEnv: PIER_PUSH_KEY     # NAME of the env var holding this repo's SSH push key
    botLogin: pier-bot            # the Forgejo account pier posts as on this server
    slackChannelId: C0123456789
    repoDir: /Users/you/pier/clones/your-repo
  - repoUrl: https://git.example.com/team/other-repo  # a different Forgejo server
    tokenEnv: OTHER_FORGEJO_TOKEN
    pushKeyEnv: PIER_PUSH_KEY     # reuse the same key by naming the same env var
    botLogin: other-bot
    slackChannelId: C0987654321
    repoDir: /Users/you/pier/clones/other-repo
slack:
  appName: pier
  botUserId: U0123456789
  appTokenEnv: PIER_SLACK_APP_TOKEN
  botTokenEnv: PIER_SLACK_BOT_TOKEN
  alertMentionTarget: "<@U0123456789>" # optional: Slack user (<@U…>) or usergroup (<!subteam^S…>) @mentioned on loud alerts
  notifications: true                  # optional (default true): set false to disable all pipeline Slack notifications

Each project selects its tracker with provider: github | forgejo (default forgejo). GitHub projects use github.com/owner/repo as repoUrl and a GitHub PAT for tokenEnv.

Per-project Forgejo fields:

  • repoUrl — full URL combining server and owner/name (e.g. https://forgejo.example.com/org/repo). Supports .git suffix and subpaths.
  • tokenEnv — name of the env var holding this project's Forgejo API token (least-privilege).
  • pushKeyEnv — name of the env var holding the SSH push key pier injects for git push (repo-scoped deploy key).
  • botLogin — the Forgejo account pier posts as on this server; its own activity is filtered out.

pier posts autonomous-pipeline status updates into each project's Slack channel — one thread per ticket, with reply_broadcast + an optional @mention (slack.alertMentionTarget) on action-required events (build failed, PR ready for review, needs-attention). Set slack.notifications: false to disable.

CLI

Run via npm run pier -- <command> (dev, through tsx) or node dist/src/cli.js <command> (built). All commands take --config <path>.

Command What it does
validate Load and validate the config + secrets, then exit.
setup Onboard each project: create the pier labels on the Forgejo repo and clone repoDir if absent.
tick One full autonomous pass: reconcile every repo through the orchestrator, dispatching builds / feedback / merges to the bounded worker pool and draining them, then exit. This is what serve's scheduler runs.
sweep A label-only reconcile pass: applies state-label transitions and failure reports via the actuator, but only logs build / feedback / merge intents without executing them (no worker pool). Useful for inspecting what tick would dispatch.
serve The long-running service: Slack grooming listener + periodic sweep scheduler. Stays alive.
install-service Render the launchd plist and write it to ~/Library/LaunchAgents/; prints the launchctl next-steps.
agent-groom Drive PI for one grooming turn directly (--repo, --prompt, optional --thread).
agent-build Drive PI for one build directly (--repo, --prompt, --workdir). Useful for the PI entry-gate check.

Running as a service (macOS, NFR-6)

npm run build
node dist/src/cli.js install-service --config /path/to/config.yaml
# then, as printed:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.pier.agent.plist
launchctl kickstart -k gui/$(id -u)/com.pier.agent

The launchd agent has RunAtLoad + KeepAlive, so it survives reboot and restarts on failure.

Development

npm install
npm test          # vitest — full suite
npm run check     # tsc --noEmit (typecheck)
npm run build     # tsc → dist/

TypeScript / Node ESM throughout: .js import extensions, import type, strict, no any.

Project layout

src/
  core/         reconcile engine, state-label model, actions, snapshot, sweep, logger
  ports/        TrackerProvider, ChatProvider, CodingAgent, ProcessRunner, FileIO
  adapters/     forgejo/ · slack/ · pi/ · process/ · fs/
  build/        executor, feedback, merge, plan re-materialization, result, redaction
  orchestrator/ worker pool, job dispatch, outcome logging
  grooming/     Slack-thread ⇄ PI grooming, /approve → planned issue
  workspace/    git worktrees, devcontainers, push-key injection, provisioning
  service/      runService (listener + scheduler), launchd plist builder
  config/       Zod schema + loader
  testing/      Fakes for every port
  cli.ts        command entrypoint
docs/superpowers/
  specs/        approved design spec
  plans/        per-phase implementation plans
  runbook-smoke.md   operator acceptance runbook

Validation

Everything automatable is covered by 217 unit/integration tests against the Fakes. The live path (real Slack + Forgejo + PI + Docker) is operator-validated via docs/superpowers/runbook-smoke.md.

Start with §2 of the runbook — the PI entry gate. Run agent-build/agent-groom against real PI and confirm src/adapters/pi/events.ts extracts PI's final text and completion correctly. That parsing is the one load-bearing assumption no Fake can verify; validate it before the end-to-end smoke.

Design docs