Skip to main content

perturb

A planning ledger for GitHub issues. When a decision made on one piece of work changes another, perturb makes sure the other one hears about it before anyone plans or builds it.

It was designed for agentic development, where every planning or implementing session starts cold, and it works just as well for people at a terminal.

The problem

Work is split into issues, and the decisions that shape them get made all over the place: while planning a neighbouring issue, in an architecture decision record (ADR), in the review of a finished piece of work. Those decisions are written down where they are made, not where they are needed.

Take a bike-share app with an epic to backfill historical data:

  • Planning #31 (backfill fares), you decide fare periods are half-open. #32 (backfill refunds) depends on fare periods, but its issue says nothing about it, so whoever plans #32 next month doesn't know.
  • An ADR decides that every refund records its grain, the period it covers (a day, a month or a year), and names #13 in its consequences. Nothing puts that in front of the person planning #13.
  • #29 was planned last week and is ready to implement. Yesterday a decision changed its scope. The implementer, often an agent with no planning context, builds the old plan.

People cope by remembering. Agents can't: each session knows the issue text and whatever it is told to read, and nothing more.

How perturb solves it

perturb keeps a ledger of events in your repository. Each one says "this changed something that may affect that issue."

flowchart LR
  decision["Decision while planning #31"] -- "perturb push" --> event
  adr["ADR consequence"] -- "perturb propose" --> event
  friction["Friction log or audit"] -- "perturb propose" --> event
  closed["Blocker closed"] -- "perturb sync" --> event
  event(["Event pending on #32"]) --> inbox["perturb inbox 32"]
  inbox --> ack["Plan for #32 absorbs it: perturb ack"]
  ack --> gate{"perturb stale 32"}
  gate -- "nothing newer" --> build["Implement #32"]
  1. Events connect a source to the issue it affects. Record one by hand with perturb push, or let perturb propose them from ADRs, from plans, and from the friction logs and audits that implementation leaves behind. Proposals wait for a person or a planning agent to confirm them, so they never turn into noise. Closing a blocker records an unblock event automatically.
  2. Every issue has an inbox. Whoever plans the issue reads it (perturb inbox 32), folds each event into the plan, and acknowledges it (perturb ack) with a note on what they did. The acknowledgement pins the exact version of the plan that absorbed the event.
  3. A stale gate stands before implementation. perturb stale 32 fails if an event arrived after the plan was last committed, or if the plan changed after it was acknowledged. Run it before building, by hand or as the first step of an agent's run.

Around the ledger, perturb reads sub-issues and blocked-by links from GitHub to answer what should be planned next (perturb next), and perturb check lints the whole thing in CI.

GitHub stays the source of truth for issues. Events are small YAML files committed next to your code and reviewed in the same pull request as the change that caused them.

Areas

Many findings are about code rather than an issue: a friction log shows a run touched src/core/fares/, or an ADR changes anything that prices a ride. An area is a named part of your codebase, declared by hand as path globs in perturb/areas.yaml:

areas:
  fares:
    paths: ["src/core/fares/**", "migrations/*fare*"]

An issue belongs to an area when it carries the area:fares label or its plan lists areas: [fares]. perturb then proposes findings about those paths, and ADR consequences that affects: [area:fares], to the open issues in that area. A handful of areas is usually enough.

Built for agents, fine for people

  • Machine-readable. Every verb takes --json and prints a stable envelope, with a reason when it refuses; gates report through exit codes.
  • Fails closed. Verbs that need GitHub sync first and refuse rather than answer from a stale cache.
  • Review built in. A proposed event does nothing until an agent's planning step or a person confirms it.
  • Guides, not prescriptions. perturb doesn't ship planning, implementing or reviewing agents. A guide for each phase shows where perturb fits and why, with example instructions to adapt into whatever prompts or skills you already use. A small agent skill, installable as a Claude Code plugin, answers "what's next?" or "is 29 still valid?".
  • By hand. The same verbs work at a terminal, and perturb propose adr:0002 --review walks you through proposals interactively.

Downsides

  • GitHub only, and online. Issues, sub-issues and blocked-by links must live on GitHub, and the graph is only as good as your use of them. Verbs that read the issue graph sync first and refuse when GitHub is unreachable; the ledger verbs (inbox, ack, confirm, dismiss) and init work offline.
  • It runs on discipline. The ledger knows only what gets pushed, confirmed and acknowledged. If decisions aren't recorded or inboxes aren't read, it quietly says nothing. Someone, or some agent's planning step, has to own that.
  • More files in your pull requests. Every event is a committed YAML file, so reviews carry ledger changes alongside code.
  • Branches blur the stale gate. stale compares when an event was created with when the plan was last committed. An event recorded on a branch that merges after someone else has planned the issue slips past the gate: it still waits in the inbox, but nothing fails. Each person also sees only their own checkout, so events on unmerged branches are invisible to inbox, stale and CI. Short-lived branches and checking an up-to-date main before implementing help.
  • Parallel work duplicates and scatters events. Duplicates are only detected within your own checkout, so two people proposing from the same ADR, or both syncing after a blocker closes, record the same event twice. Because a sync can write unblock events, they can land in unrelated pull requests. And proposals need a clear owner, or they pile up unreviewed.
  • Proposals are heuristics. Routing uses #N mentions and path globs in a hand-maintained areas.yaml. Loose areas mean noisy proposals to dismiss; missing areas mean findings that reach nobody. How well the rules hold up on real projects is not yet measured.
  • The stale gate is coarse. Any pending event newer than the plan's last commit marks it stale, however small the change, and any edit to an acknowledged plan, even a typo fix, means acknowledging its events again.
  • It assumes a way of planning. Plans must be Markdown files with closes: front-matter, and issues need the ready-to-implement label. Plans kept in issue bodies, documents or tickets elsewhere don't count.
  • Some sources need specific formats. ADR proposals need the structured ADR format (perturb adr migrate converts prose ADRs). Friction and audit proposals need files in the run evidence format. Your workflow has to write them, though a friction log only needs a list of commits.
  • One repository at a time. There is no ledger across repositories.
  • Early. Formats and JSON output may change before 1.0, and it isn't published to PyPI.

Install

You need Python 3.11+, git, a repository hosted on GitHub, and the gh CLI logged in (or GH_TOKEN set). If you use a wrapper around gh, point PERTURB_GH at it.

perturb is on PyPI:

uv tool install perturb      # or: pipx install perturb

Quick start

cd your-repo
perturb init          # creates perturb/: areas.yaml, config.yaml, README.md, events/
perturb next          # ready, unplanned issues, the ones that unblock the most first
perturb inbox 32      # what has changed for #32

# Planning #31, you lock a decision that affects #32:
perturb push --from 31 --to 32 --kind decision "Fare periods are half-open"

# Planning #32: fold the event into the plan, commit the plan, then acknowledge it.
perturb ack 32 --all --plan tasks/backfill-refunds.md --note "Design decision 1"

perturb stale 32      # exit 0: the plan has seen everything
perturb check         # the same checks, for CI

Commit the perturb/ directory along with your work; perturb init has already added its local cache, .perturb/, to .gitignore. To run the checks on every pull request, see Getting started.

A plan is a Markdown file with closes: <issue number> in its front-matter, at tasks/<slug>.md by default. An issue counts as planned once its plan exists and the issue carries the ready-to-implement label. Both are configurable in perturb/config.yaml, along with where friction logs and audits live; declare your codebase's areas in perturb/areas.yaml so friction reaches the issues that touch the same code.

Status

Early. Version 0.0.1 does everything described here and is used to build perturb itself, but the JSON envelope and file formats may still change.

Documentation

Contributing

See CONTRIBUTING.md for development setup and how this repository uses perturb to build perturb.

License

MIT

Metadata

Release files for perturb 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for perturb 0.0.3
File Size Uploaded Provenance Actions
perturb-0.0.3.tar.gz 101.3 kB With provenance Details for perturb-0.0.3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for perturb 0.0.3
File Interpreter ABI Platform Actions
perturb-0.0.3-py3-none-any.whl Python 3 none any Details for perturb-0.0.3-py3-none-any.whl

Total release size: 154.3 kB

Release files / perturb-0.0.3.tar.gz

Download URL perturb-0.0.3.tar.gz
Size 101.3 kB
Tags Source
SHA-256 checksum
How to use checksums
5ecb7a833c454bd8aa1f598d172b5ef3895f9a4cc1282debaf6b39eeb93fbcd1
BLAKE2b-256 checksum
How to use checksums
ad8ffea2ad3d5724f5c0863b59842c32fea83ae7c4c5d400890e29e297a559fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / perturb-0.0.3-py3-none-any.whl

Download URL perturb-0.0.3-py3-none-any.whl
Size 53.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
94aec5e7d5de73a9f9a4687e4dbd4735380bf4c86b584626cf751257f7566601
BLAKE2b-256 checksum
How to use checksums
341f1d2adf835ec7bf1d48213588986b32d0cfd44841585ac50fae0f3b93e782
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Security for perturb 0.0.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for perturb-0.0.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page