gitbulk design notes¶
Captured from the design conversation that produced the Phase 0 scaffold and the Phase 1A foundations session.
Authoritative source: As of 2026-05-27,
../this.iis the source of truth for design decisions. This file is the narrative explainer that points into it. Whenthis.iand this file disagree,this.iwins.Node ids (e.g.,
7mxr4pql) below cross-reference intothis.i. Seemethodology.mdfor what intent nodes are and why they exist as a separate layer above the code.
This is not a spec. AGENTS.md is the contract; this.i is the
"why we chose what we chose" record; this file is the human-readable
walking tour of both.
1. Problem statement¶
The user contributes to ~150 git repos (most under provenant-dev, some
open source). Open PRs across that set are too many to triage by hand
each day. gitbulk runs unattended (typically from cron) and:
- reports the state of those PRs (ready-to-merge, blocked-by-CI, awaiting-human-response, awaiting-bot-response, conflicts, non-default-base, stale, etc.);
- can launch headless Claude Code agents against PRs matching a filter, with a pluggable prompt;
- can rebase the user's PRs onto their repo's current default branch;
- can auto-merge PRs that pass a per-repo policy and have aged past a threshold;
- can close PRs that have gone stale.
All of this must be safe to run concurrently with active development on the same local clones.
2. Configuration model¶
Two files, defaulting to ~/.config/gitbulk/.
repos.txt — plain owner/repo per line. # comments and blank
lines ignored. Local clone resolved as ~/code/<basename(repo)> unless
--code-root overrides. Format chosen for minimal friction; richer
formats (YAML, inline tags) were considered and rejected for v1.
gitbulk.yaml — policy and classification. See
config/gitbulk.yaml.example. Highlights:
defaults.merge_policy—strict(approval + green CI + clean + default-branch-target + age),ci-only(drops the approval gate), ornever.defaults.min_age_days— auto-merge age threshold. Default value TBD (see open questions).defaults.stale_age_days/stale_cooloff_days— close-stale knobs.humans.org,humans.exceptions,humans.always_human— see classification model below.bots:— known non-human accounts.repos:— per-repo overrides for any of the above, plus per-reposkip_checksandextra_checks.
3. Humans-vs-bots classification¶
Unknown accounts default to non-human, because the set of bots will grow over time and silently treating a new bot as human would be the wrong failure mode.
Resolution order, evaluated per comment/review author:
login in always_human→ human.login in bots→ non-human.login in org_members AND login not in exceptions→ human.- Otherwise → non-human.
Org members are fetched once via gh api orgs/<org>/members and cached
for cache_ttl_hours. CCR, Dependabot, GitHub Actions, etc. are
seeded in the example config.
4. Subcommands¶
| Subcommand | Purpose |
|---|---|
report |
Structured + human-readable state of every open PR across configured repos. |
summarize |
Run Claude over the latest report to surface what most needs attention. |
dispatch |
Launch headless claude -p per matching PR, inside a worktree, logs captured. |
merge |
Auto-merge PRs that pass per-repo policy. Default --dry-run. |
rebase-onto-default |
Rebase user's PRs onto their repo's default branch. Default --dry-run. |
close-stale |
Close inactive PRs with a configurable message. Default --dry-run. |
show |
Cat the latest summary of a given subcommand's run. |
ack |
Clear the ATTENTION sentinel after review. |
invariants |
List the invariant registry and which subcommands use each. |
Every mutating subcommand defaults to --dry-run and requires --apply
to act. A misconfigured cron entry must not silently mutate state.
5. Local-git safety contract¶
The most important invariant in the tool. Restated from AGENTS.md so this doc is self-contained:
- Never modify the working tree, index, or HEAD of any clone under
~/code/. - Any operation that requires a checkout creates a
git worktreeunder a disposable root (/tmp/gitbulk/by default — see open questions) and cleans it up infinally. - Before writing inside a worktree, verify the path actually resolves under the worktree root and is not the main clone (defensive against worktree-creation bugs).
- Read-only
git -C <path> <subcmd>is fine for status/config/log/etc.
6. Concurrency¶
Two gitbulk processes must be safe to run at the same time.
- Global advisory lock (
~/.cache/gitbulk/run.lock,fcntl.flock): shared for read-only subcommands, exclusive for mutating ones. Multiplereportruns may overlap; amergewaits for reports to finish and vice versa. - Per-repo lock (
~/.cache/gitbulk/locks/<owner>__<repo>.lock): held for the duration of any mutating op on that repo. Lets one process merge repo A while another reports on repo B. - Run state (
~/.cache/gitbulk/runs/<timestamp>-<subcommand>/): durable record of decisions, survives crashes, supports a future--resume.
7. Invariants framework¶
Each subcommand runs an ordered chain of named invariants before
acting. An invariant is a small function
(repo, pr_or_none, context) -> Pass | Skip(reason) | Fail(reason):
- Pass → proceed.
- Skip → skip this repo/PR, log reason, continue with others.
- Fail → abort the whole run (structural problem: bad auth, etc.).
All invariants live in a registry. gitbulk invariants and
gitbulk <subcmd> --list-checks print them. Cmdline --require NAME
and --skip-check NAME add or suppress individual invariants for a
single run. Every suppression is logged into run state with a WARNING
so an audit can find every loosened operation.
Default catalog¶
Universal preflight (once per run):
- gh.authenticated
- config.parseable
- org.members.fresh
Per-repo preflight (every subcommand):
- local.exists
- local.remote_matches
- local.default_branch_in_sync
- github.reachable
- github.not_archived
Per-PR baseline (report, summarize, dispatch, merge, rebase, close):
- pr.base_is_default
- pr.author_known
Mutating only (merge, rebase, close, dispatch):
- local.no_uncommitted_in_pr_branch
- local.recent_push_quiescence
- repo.not_in_deny_list
Merge-only:
- pr.mergeable_state_clean
- pr.required_checks_green
- pr.approved_per_policy
- pr.no_blocking_label
- pr.age_threshold
- pr.no_unresolved_threads
Rebase-onto-default-only:
- pr.author_is_me
- pr.no_automerge_pending
- pr.force_push_allowed
Close-stale-only:
- pr.inactive
- pr.previously_warned (refuses to close on first sight; needs
persistent state across runs)
Dispatch-only:
- repo.agentprep_verified — runs agentprep verify in the clone
- repo.agentprep_initialized
- prompt.exists_and_nonempty
- system.resources_available
8. Notification & error visibility (layers 1–4 in v1)¶
v1 ships the file-based layers only. External adapters (ntfy.sh, slack, desktop notifications) are deliberately deferred.
Layer 1 — structured run artifacts:
~/.cache/gitbulk/runs/<timestamp>-<subcommand>/
state.yaml # full run state — every repo, every decision
summary.md # human-readable summary
errors.log # warnings and errors only
invariants.log # every skip/fail with reason
manifest.yaml # subcommand, config, flags, version
~/.cache/gitbulk/runs/latest-<subcommand> always point at
the newest run of that subcommand.
Layer 2 — exit codes:
- 0 nothing to flag
- 1 structural failure (bad config, gh not authed, network)
- 2 ran successfully but PRs need user attention
- 3 ran successfully but at least one repo skipped by an invariant
- 4 ran with --skip-check overrides applied (audit signal)
- 99 subcommand not implemented (Phase 0 scaffold sentinel)
Layer 3 — dashboard: ~/.cache/gitbulk/dashboard.md rewritten on
every run with a single-screen view of the most recent state across all
subcommands. Designed to be cat-able at shell start.
Layer 4 — ATTENTION sentinel: ~/.cache/gitbulk/ATTENTION created
when exit code is 2 or 3. Shell-prompt or tmux-statusline integration
shows a glyph while it exists. It clears implicitly when you view the run
that raised it (gitbulk show <sub> matching subcommand + run id), when
you view the dashboard (bare gitbulk show), or when a later clean run of
the same subcommand supersedes it. gitbulk ack is the explicit
catch-all that removes any sentinel unconditionally. See this.i node
aklr5pq3.
Cron wrapper convention: bin/gitbulk-cron captures all stdout/stderr
to ~/.cache/gitbulk/cron/<timestamp>-<subcommand>.log and symlinks
last-failure.log on non-zero exit.
9. Phase plan¶
- Phase 0 — Scaffold (done): repo init, AGENTS.md, package skeleton, argparse shell, smoke tests passing.
- Phase 1 — Core infra (no network): config loader, invariant
registry + chain runner, run state, dashboard, ATTENTION sentinel,
ack, exit-code wiring, locking. All unit-tested, network-free. - Phase 2 — Read-only GitHub:
ghwrappers, human/bot classifier with cached org members, worktree helper with path verification.reportworking end-to-end.invariants+--list-checksintrospection. - Phase 3 —
summarize: consume Phase 2 report runs, invokeclaude -p, write LLM output into the run dir. - Phase 4 —
dispatch: per-PR worktree, headlessclaude, log capture, concurrency cap of 2 (matches user's subagent rule).repo.agentprep_verifiedwired in. - Phase 5 — Mutating subcommands:
rebase-onto-defaultfirst (teaches the push/lease patterns), thenmerge, thenclose-stale(which depends on persistent state for thepr.previously_warnedcooloff). - Phase 6 — Polish:
gitbulk show, README crontab examples, end-to-end docs.
Each phase is its own commit (or small PR-equivalent set), with TDD discipline per phase.
10. Out of scope for v1¶
- A web UI.
- External notification adapters (ntfy, slack, desktop). Layers 5+ in the design — explicitly deferred.
- ~~A bundled single-file executable like agentprep ships.
pip install -e .is fine for v1.~~ Superseded 2026-05-29 (this.inodedstbr5kq): gitbulk now ships a hybrid distribution — it stays pip-installable AND ships a single self-contained zipapp fetched from a GitHub release, with agitbulk installself-installer and a notice-onlygitbulk update. See the Install & Distribution subtree inthis.i. - A GitHub remote for this repo. Local-only until the user runs
gh repo createthemselves. - Cron file installation. The user wires their own crontab around
bin/gitbulk-cron.
11. Resolution status of the Phase-0 open questions¶
Phase-0 closed with seven open questions. All seven now have positions
recorded in this.i — six as decisions, one as a deferred tension:
| # | Question | Resolution | this.i node |
|---|---|---|---|
| 1 | Default min_age_days for auto-merge |
3 business days (M–F, local TZ, no holidays) since ready_since; "ready" itself is stricter than GitHub-clean (bot threads block too) |
bg4pqn7m + zk3r4nqp |
| 2 | Worktree root location | ~/.cache/gitbulk/worktrees/<runid>/<owner>__<repo>/ — survives reboot for crash forensics, XDG-conventional |
mw6kp2nq |
| 3 | summarize prompt design |
Deferred to Phase 3 entry — depends on report's structured output, which doesn't exist yet |
kw2pn7qz (tension) |
| 4 | rebase-onto-default UX on conflicts |
Keep the conflicted worktree; write CONFLICT.md with fix-up commands; gitbulk gc skips worktrees in conflict state |
vp7n2krq |
| 5 | Per-repo policy precedence | Cmdline always wins. Relaxing (--skip-check) trips exit 4 + WARNING; tightening (--require) logs INFO only |
r4nzp7kq |
| 6 | gh rate limiting |
Serial per-repo + GraphQL coalescing, no limiter in v1 (~300 calls vs 5000/hr budget) | gd4kp7nz |
| 7 | Missing local clone | Skip-with-warning, scoped to subcommands that need a clone; never auto-clone, never fail the whole run | 5xqp2nkr |
New decisions and tensions opened during the Phase-1A session¶
| Topic | this.i node |
Status |
|---|---|---|
| Personal account owns the public repo (dhh1128/gitbulk, not provenant-dev) | 6xp4kq2n |
decision |
| Local repos are first-class citizens — fleet = (repos × PRs), not just PRs | xq4npk7r |
decision |
Methodology adoption (this.i, speculative interview, gates, adversarial review) |
nh4kp2rq |
decision |
100% branch coverage on src/gitbulk/, enforced in CI |
cn4pk7zq |
decision |
dispatch execution kernel — subprocess multiprompt, shared kernel, or reimplement |
mp7kn4qz |
tension (Phase 4) |
| Multiprompt packaging future — multiprompt's own this.i / CI / release story | fw5kq6np |
tension (Phase 4) |
| Scan and findings artifact convention — format, location, lifecycle | ck7n4pqr |
tension (Phase 4) |
| Repo cleanup subcommand scope — worktrees, branches, refs | jw3kpn4q |
tension (Phase 5/6) |
| Default branch rename handling | rj7p4kqn |
tension |
When in doubt, bias toward "skip with reason logged" over "do something risky."