IonOrg steward
00 // Infographic abstractBuilt · Apache-2.0Approval before structure

A steward bot for a GitHub organisation

Ion reads the whole org. It changes nothing without your yes.

Ion watches every repository in your organisation. It spots drift from your policy, finds repos that belong together or apart, and writes up each change as an exact plan: the evidence, the commands it would run, the way back, and how much of the org it would touch. Then it stops. A maintainer approves that exact plan, or nothing happens. Only then does ion write, through capped, logged requests, with a circuit breaker that halts any run touching too much at once. Archiving a repo always needs its own yes. Ion never deletes, never force-pushes, and never merges its own pull requests.

0
structural changes without a maintainer’s approval. The gate is checked in code, on every run, dry runs included.
The rule
6
stages: collect, analyze, propose, approve, execute, report. Only the fifth writes.
Pipeline
9
kinds of proposal, from merge and split to policy fix and fleet-wide autofix.
SPEC §2.3
25%
of the org in one run trips the circuit breaker, before anything is written.
Default guard
01 // Why an org driftsThe problem

Repos multiply. Nobody is watching the shape of the whole.

Each repository gets care. The organisation around them does not, because no person has the whole map in their head.

Drift

Policy fades repo by repo

Branch protection missing here, secret scanning off there, a public repo with no licence. Each gap is small. Together they are the org’s real posture, and nobody chose it.

Duplicates

Two repos, one project

Code copied between repos, commits landing in both on the same days, readmes pointing at each other. The split made sense once. The evidence that it stopped making sense sits unread in the history.

Stale weight

Finished work never gets put away

Experiments and one-off tools stay active for years because archiving feels risky and nobody owns the decision. New people cannot tell what is alive.

Fear of the fix

Cleaning up by hand is scary

Merging two repos without losing history, splitting one safely, proving nothing was dropped: doable, fiddly, and easy to get subtly wrong. So it does not happen.

Ion’s answer is a division of labour. The machine does the watching, the measuring and the careful writing. People keep the deciding. The next panel is that bargain as a working model.

02 // The pipeline, in your handsWorking modelYou are the maintainer

Read widely. Write narrowly. Wait in the middle.

Every ion run walks the same six stages. The first three only read and write words. The fourth belongs to a person. The fifth is the only stage that can change anything, and it re-checks the fourth before it moves. Run the model, then try to break the rules: execute without approving, approve and then change the plan, or widen the blast radius past a quarter of the org.

Plannone yet
Fingerprintplan-sha256: … Approvalno approval recorded
6 of 80 repos (8%) amber mark = 25% guard

A teaching model with invented repos and a short stand-in fingerprint. The rules are ion’s real ones: approval binds to the plan hash (SPEC §2.4), the breaker guards are the shipped defaults (guide), and the executor re-derives approval from the record instead of trusting a stored flag.

Collect

One query, local mirrors

Metadata comes from one batched GraphQL query under a points budget. File contents come from bare git mirrors on the machine, never from repeated API calls.

Analyze

Counting, not guessing

Health, policy drift, coupling, cohesion, lifecycle and tag boundaries. All deterministic. A language model may later help word a proposal. It cannot invent one, add a repo, or raise a score.

Report

One dashboard, edited in place

An “Org health” issue lists what waits, what is queued, what is held and what just finished. Ion edits it in place and never comments. A short digest goes to your notifiers, only when something changed.

03 // Approval is for one planMaintainer onlySPEC §2.4

A yes means: yes to these commands, seen by this person.

Each proposal is one issue in a private tracker repo. Its body holds the evidence, the ordered steps, the exact commands, the rollback, the blast radius, and a fingerprint of the plan: plan-sha256: …, a hash that changes completely if one command changes.

A maintainer can answer in three ways. All three are checked the same way: who acted, whether they maintain every repo involved, and which fingerprint was on display when they acted.

Channel 1

A label on the issue

Add ion:approved, ion:rejected or ion:hold. Ion reads the issue timeline to find who applied it. A decision label from a non-maintainer is recorded as ignored and taken off again, with no comment.

Channel 2

A signed bus message

Send ion approve P-0007 over the Divan Bus from an approved seat. The signature is verified, then ion mirrors the decision onto the issue as the label, so the public record stays on GitHub.

Channel 3

A tick on the dashboard

Tick the approve or hold box on the Org health issue. Ion checks the edit history: every edit since its own last write must be by a maintainer, or the tick fails closed and counts for nothing.

Change the plan and the fingerprint changes. The old yes stops counting, the label comes off, and ion asks again.

  • Approvals expire after 14 days by default. The label comes off then too.
  • You can require more than one approver, in total or per proposal kind.
  • A rejected idea stays quiet for a cooldown period, unless its score rises sharply.
  • Archiving always needs its own approval, even right after an approved merge.

This reviewer-shaped gate is the heart of ion, and it is why the project sits with the amp team: the work is reviewing. The machine prepares a case worth deciding. A reviewer decides it. See why the name fits.

04 // How writes stay safeEnforced in codeSPEC §2.5

Approved is necessary. It is not sufficient.

Even a fully approved run passes through four more checks before and during writing. Each one can stop it without touching anything.

Typed writes

Every write is a named request

One file, ion/writes.py, is allowed to call a mutating GitHub API. A test enforces that. Each request has a known type, a checked shape, and a per-run cap. Over the cap, work waits for the next run instead of failing. Pushes may only target ion/* branches, never a default branch.

5 PRs · 1 archive

Default caps per run for the two loudest write types. Yours to set.

Circuit breaker

A run that looks wrong stops itself

Before writing, the run totals its own blast radius. During the run, every write is checked again. Trip any guard and the run aborts, says so in the digest, and records the trip where ion status shows it.

25% · 2 · 150

Defaults: share of repos touched, archives per run, total writes. Any branch-protection weakening also trips it.

Exact verification

Moves are proved, not counted

A merge is accepted only when the imported tree matches the source tree exactly and the source tip is an ancestor of the result. A split must match the rewrite tool’s commit map, commit for commit, plus tree equality. Every moved commit carries an Ion-Origin: org/repo@sha trailer saying where it came from. Counting commits is not a check, so ion never does it.

Ledger and kill switch

Stop anytime, resume cleanly

ion pause stops every executor and tracker write. Reads still work. Each step writes a before and after row, so a crashed or interrupted run resumes without repeating finished steps. ion explain P-0007 shows the whole history of one proposal.

Underneath all of it: nothing irreversible. No deleted repos, branches or history. No force push, in any spelling, ever. Archive instead of delete. A maintainer merges ion’s pull requests. Ion never merges its own.

GuardDefaultWhat happens when it trips
Share of org touchedover 25%Run aborts before writing. Digest and status record it.
Archives in one runover 2Run aborts. Archiving still needs its own approval each time.
Total writes in one runover 150Run aborts. Per-type caps usually defer work long before this.
Weaker branch protectionanyRun aborts. Ion may strengthen protection. It may not lower it.
Kill switchion pauseAll executors and tracker writes stop. Reads continue.

Defaults from the shipped guard config. Every value is yours to change in ion.yml, and ion plan-config previews what a change would do before you adopt it.

05 // What ion proposes9 kindsSPEC §2.2–2.3

Nine kinds of proposal, each with its evidence attached.

A candidate must persist across repeated snapshots before it becomes a proposal, so one odd day never files an issue. Structural kinds carry ordered steps: a later step refuses to run until the pull request before it has landed.

KindWhat it meansTypical evidence
mergeFold repo A into repo B, full history kept, then a pointer PR in ACoupling score and its five signals
splitMove a directory out into a new repo, history kept, removal PR after proofSeparable clusters inside one repo
move_codeMove code between existing repos, with the same two exact checksImports and co-change across the pair
archivePut a finished repo away. Always its own approvalStaged lifecycle: active, stale, archive-proposed
rename · new_repoName and homekeeping for splits and mergesThe parent proposal that needs them
policy_fixBring a repo back to policy: protection, alerts, scanning, required filesDrift from ion.yml, with an action level per class
health_fixMissing readme, licence, CI or descriptionPer-repo health checks
autofixOne small change across many repos, as a single proposal of exact diffsion autofix preview diffs over the mirrors

How “these two belong together” is scored

Five signals per repo pair, combined so that one saturated signal alone just reaches the merge threshold of 0.6. Weak signals need company. The name-prefix signal is deliberately tiny: repos called protean-* share a naming scheme, not a life, and ion’s first live run proved the point with 265 pairs that were mostly that.

SignalWeightHygiene
Cross-references0.6Manifests, imports, git URLs, readme links
Duplicated code0.6Identical files plus near-duplicates. Vendored, lock and generated files ignored
Co-change0.6Shared commit days over average commit days. Huge commits, bots and ion itself excluded. Half-weighted unless a content signal agrees
Shared dependencies0.3Overlap of the two dependency sets
Shared name prefix0.05Counts only when another signal is already at least 0.15

Tag rules add boundaries: mark repos with topics such as scope:app, say which scopes may depend on which, and cross-scope merges are blocked or flagged before they are ever proposed. Two fleet tools ride on the same mirrors: ion grep searches every repo with no API calls, and ion autofix previews a change everywhere, then files one proposal for exactly those diffs.

06 // StatusSPEC §7–8

What is built, what is not, and what will never be.

Built and tested

  • The full six-stage pipeline, offline tests end to end: merge, split, move and autofix run against local bare repos
  • Plan-hash approval on all three channels, with expiry and quorum
  • Typed writes with caps, circuit breaker, kill switch, resumable ledger
  • Exact move verification and provenance trailers
  • Org health dashboard, digests, fleet grep and autofix, config preview
  • Phase 1 deployment: a daily scheduled run on a Mac, executors in dry-run until you add --apply

Not yet built

  • Splits that prefer cuts along tag boundaries. Tags are per-repo today, so there is nothing finer to cut along
  • Importing several branches or tags on merge. Default branch only
  • Pinning the dashboard issue automatically. Pin it once by hand
  • Later survey ideas: path-level owners, scorecard input, conditional rules
  • Phase 2: a GitHub App identity on a hosted schedule. Config changes only, when uptime justifies it

Out of scope, by design

  • Deleting repos, branches or history. Force pushes
  • Merging its own pull requests
  • Reviewing other people’s pull requests, or writing feature code
  • Team and membership management
  • Webhooks or a live server. One scheduled pass a day is the design

Run the checks yourself: uv run pytest -q and uv run python scripts/check_licenses.py. The licence guard fails the build if any dependency falls outside MIT, BSD, Apache-2.0, ISC, PSF or MPL-2.0. No GPL dependency, ever.

07 // Why ion ἰόν · going

An ion is something that goes, along a path, carrying charge.

Michael Faraday coined the word in 1834, from the Greek ἰόν, “going”, for charged particles that move through a solution toward an electrode. Four threads tie the name to the work:

Going

It moves through every repo

Ion travels the whole org on each run and carries a signal from where the evidence sits to where the decision happens.

Lattices

It holds the structure

In a crystal, ions sit on the points of a lattice and the pull between them holds it together. Ion watches how repos relate and keeps that structure sound. The lattice is also home ground: it runs in the same family as the incentre lattice and Exo.

Charge

It acts only along approved paths

Charge does work only along a path. Ion’s paths are the three approval channels. Off those paths, no current flows and nothing changes.

The amp team

Current is ions in motion

Current is measured in amperes, and ion is owned and reviewed by the amp team, whose craft is reviewing. That fits the design: ion’s centre of gravity is a review gate. In the live Aska Digital deployment, proposals are prepared for a reviewer’s yes or no, and the reviewer’s word is what turns a plan into current.

08 // Prior art, and where to go nextIdeas credited, code original

Standing on published designs. Copying none of them.

Ion’s mechanisms come from other projects’ documented behaviour, re-implemented in its own words. Nothing is vendored. The typed-write broker follows GitHub Agentic Workflows’ safe outputs and actions/stale’s per-run caps. The breaker follows Peribolos. Plan-bound approval follows policy-bot. The dashboard follows Renovate’s layout. Coupling follows Adam Tornhill’s change-coupling method. The full credit table names all twenty-five, each with its licence, checked on 2026-10-13.

Security problems go through private reporting, described in SECURITY.md. Be kind, per the code of conduct.