ion grep
Search the whole org, free
Runs git grep over the default branch of every local mirror. No API calls, no writes. The fastest way to answer “where else does this pattern live?” before you propose changing it.
Guide
Ion’s executors dry-run unless you pass --apply. Your first day with it should end with a dashboard full of proposals and an org that looks exactly as it did that morning. Approve later, on purpose, one plan at a time.
You need Python 3.12 or newer, uv, git with git subtree, the GitHub CLI gh logged in to an account that can see the org, and git-filter-repo if you want splits.
git clone https://github.com/aska-digital/ion.git && cd ion uv sync # add --extra graph for Louvain clustering cp ion.example.yml ion.yml # then edit: org, maintainers, tracker_repo uv run ion --config ion.yml doctor
doctor checks the GitHub login, git and subtree, filter-repo, the language-model backend, the bus if you use one, and the config itself. Fix what it names. Then take the first full pass. It reads, measures, files proposals in the tracker, and writes nothing to any other repo:
uv run ion --config ion.yml run # the whole pipeline, executors dry-run uv run ion --config ion.yml status # what is waiting, held, done; any breaker trip uv run ion --config ion.yml dashboard # the Org health issue, or --print to just read it
State lives in ~/.ion/state.db and repo mirrors in ~/.ion/cache. Set ION_HOME to move both, and ION_CONFIG to set a default config path. Your real config, with maintainer names and local paths, belongs outside any public repo: ~/.ion/ion.yml is a good home.
Keep the tracker repo private. Proposal bodies name your private repos and quote their metadata. The default tracker name is ion-tracker. Create it as a private repo before the first run.
Every repo can override the org config in its own .github/ion.yml: who maintains it, whether ion governs it at all, and its policy action levels. Two settings shape your first month more than any others:
policy_strategy: opt-out (the default) governs every repo unless that repo says enabled: false. Choose opt-in to govern only repos that ask.policy_actions sets, per policy class, what a drift finding does: log records it, issue proposes it and waits for approval, fix applies it unattended. Start everything at issue. Promote a class to fix only after its proposals have bored you for weeks. Structural kinds can never be fix.| Key | Meaning |
|---|---|
org | The organisation to steward |
maintainers | GitHub logins whose approval counts. Per-repo lists refine this |
tracker_repo | Private repo where proposals are filed as issues |
approval_sources | Any of github_label, divan_bus, dashboard |
notifiers | stdout, file, divan_bus, github_issue, webhook. Failed deliveries queue and retry |
llm.backend | none (default), claude_cli or anthropic_api. Words only: the model cannot add repos or raise scores |
guards | The circuit breaker: max_repo_fraction, max_archives, max_writes, forbid_protection_lowering |
writes.caps | Per-run cap by write type, for example {open_pr: 5, archive_repo: 1} |
quorum · approval_ttl_days | Approvers required per proposal, and when approvals expire (14 days) |
lifecycle | dormant_days, plus repos or a topic that freeze the stale clock |
boundaries | Tag rules and scope prefix. Cross-scope merges blocked by default |
budget.graphql_points_per_run | Collection stops cleanly at the budget (500) and resumes next run |
Before adopting any change, preview it. ion plan-config NEW.yml runs both configs over the same snapshot and reports candidates gained and lost, drift changes, repos that would opt out, and open proposals that would be re-planned. It writes nothing.
uv run ion --config ion.yml plan-config candidate.yml uv run ion --config ion.yml plan-config --set thresholds.merge_coupling=0.7
[P-0007] merge a into b in the tracker repo, labelled with its kind and state. Ion edits this one issue in place for the proposal’s whole life.plan-sha256: … line is the plan you are about to endorse. Your approval will count for that hash and no other.ion:approved, ion:hold or ion:rejected; or send ion approve P-0007 over the bus; or tick the box on the Org health issue. You must be a maintainer of every repo the plan touches, or your label is removed and logged as ignored.ion execute --apply P-0007 re-derives your approval from the record, checks caps and the breaker, then works through the steps in order. A dependent step waits until the pull request before it has landed.ion/* branches and proves the move before asking. It never merges its own work. If the plan ends with archiving the source repo, that arrives as a separate proposal needing a separate yes.# What did ion do, and why does it believe it may? uv run ion --config ion.yml explain P-0007 # ledger, approvals, steps, states uv run ion --config ion.yml status # queue, holds, last breaker trip
If new evidence changes a plan after you approved it, expect ion to ask again. The label comes off by itself. That is the system working, not a fault to work around.
ion collect | analyze | propose [--dry-run] | sync-approvals | execute [--apply] [P-0007]
ion report | run [--apply] | status | explain P-0007 | pause | resume | doctor
ion grep PATTERN [--path GLOB] [--repo GLOB] [-i] [-F]
ion autofix [NAME] [--repo GLOB] [--propose]
ion plan-config NEW.yml (or --set key=value)
ion dashboard [--print]
ion grep
Runs git grep over the default branch of every local mirror. No API calls, no writes. The fastest way to answer “where else does this pattern live?” before you propose changing it.
ion autofix
With no flags it prints the diffs and writes nothing. With --propose it files one proposal whose fingerprint covers exactly those diffs. Pull requests exist only after approval, each recomputed and compared with what was approved. Built in: a readme skeleton and a Dependabot config generator. Write your own by subclassing ion.fleet.Autofixer.
ion plan-config
Compares two configs over the same snapshot: candidates added, removed, rescored or blocked, drift and lifecycle changes, proposals that would re-plan. Tune thresholds with evidence instead of vibes.
ion pause
Creates ~/.ion/PAUSED. Every executor and tracker write stops until ion resume. Reads, grep and reports keep working, so you can investigate with the machine’s hands visibly off the controls.
Phase 1 is a LaunchAgent on a Mac. The installer renders the job file and deliberately does not load it, so you read it first:
deploy/install.sh ~/.ion/ion.yml launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.aska.ion.plist
It runs ion run daily at 06:30 and logs to ~/.ion/ion.log. The shipped job keeps executors in dry-run. Add --apply only after a few weeks of proposals you would have approved anyway, and keep the source tree outside ~/Documents to avoid macOS privacy prompts. Any scheduler that can run one command a day works as well.
Phase 2, when cost or uptime justify it, swaps the laptop for a GitHub App identity and a scheduled GitHub Action with llm.backend: anthropic_api. Only config changes. The pipeline, gates and guards do not move.
plan-config until the noise is gone.explain after each.fix. Leave archives manual forever, because ion will.