# Birdseye

> Sweep every git repo under a workspace root and report true working state — uncommitted work worth committing, branches that should merge, and commits that should push. Verifies remote state through the GitHub API rather than stale tracking refs, and, if an agent runner writes a run report, judges agent branches by their reviewer verdict instead of guessing. Trigger with /birdseye [root], or when the user asks "what's the state of everything / anything need merging or pushing / did I leave work uncommitted."

- Skill: `davemaynard/birdseye` (Agent Skill)
- Install (CLI): `npx skillmds@latest add davemaynard/birdseye`
- Raw SKILL.md: https://api.skillmd.com/api/skills/davemaynard/birdseye/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: davemaynard (https://skillmd.com/u/davemaynard)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/davemaynard/birdseye

---


# /birdseye — the whole workspace, honestly

**Requires:** `git`, plus the GitHub CLI (`gh`) authenticated — `gh auth status`
should pass. Remote state is read through the API because local tracking refs go
stale, so without `gh` the push/merge answers are guesses.

Workspace root: `$ARGUMENTS` if given, otherwise the directory the session was started from.

Walk every git repo under the root and come back with **one action list**, not a status dump.
The deliverable answers three questions per repo and nothing else:

1. Is there uncommitted work that *should* be committed? (vs. build artifacts that should be ignored)
2. Is there a branch that *should* be merged? (vs. one that is deliberately waiting)
3. Is there a commit that *should* be pushed?

Anything with no answer to all three gets one line in a "clean" table. Resist writing paragraphs
about healthy repos — the value here is the short list of things that need the user's hands.

## What this is NOT

- **Not a portfolio review.** No PUSH/HOLD/PARK/KILL, no "should this exist." That's a monthly
  exercise. This is mechanical repo hygiene and can run any day.
- **Not a code review.** Judging *whether the code is good* is out of scope. Judging whether it
  **builds and passes tests** is in scope, because that's what decides mergeable vs. needs-a-human.
- **Not a fixer, by default.** Report first, then execute the items the user picks.
  **Pushing is in scope.** One "go" on an action list authorizes the whole list — commits,
  merges, branch deletions, *and* the pushes that finish them. Do not split pushes out into a
  second approval round: a repo left with local-only commits at the end of a sweep is an
  unfinished sweep, and re-asking per repo is the friction this skill exists to remove.
  Push to each repo's **own** default branch, which is not uniformly `main` (see Phase 2).
  Still the user's call, and worth pausing for: a force-push, a push that rewrites published
  history, or anything touching a repo they didn't name.

## Phase 0 — Enumerate

```bash
ROOT=<workspace root>    # the argument above if given, else the session's starting directory
for d in "$ROOT"/*/; do [ -e "$d/.git" ] && echo "$d"; done
```

Include notes/docs repos if any live under the root (they are real repos and do get left dirty).
Note non-git dirs separately and briefly — a directory of worktrees belongs to its parent repo,
not an orphan, and a loose `scripts/` folder is just files. Don't propose `git init` for either.

## Phase 1 — Local state, one pass

Per repo collect: current branch, last commit (short + date), `git status --porcelain` count and
contents, `git branch -vv`, stash count, `git worktree list`.

**Do not trust `git status -sb` ahead/behind.** It compares against remote-tracking refs that can be
days stale, and it will report "in sync" for a repo whose remote has moved. Phase 2 exists for this.

## Phase 2 — True remote state (the part that matters)

**Push/fetch works — check, don't assume.** Start with `ssh-add -l`. If it lists an identity, plain
`git push` / `git fetch` / `git ls-remote` all work normally and you need none of the workarounds
below. Only when it reports *no identities* is anything special required, and even then the fix is
the user running `ssh-add ~/.ssh/<key>` interactively (see Phase 6) — ten seconds, not an outage.

Fallback when the key is locked and you can't ask: `gh` holds its own keyring token, so borrow it.
Note that a global `url.git@github.com:.insteadOf https://github.com/` rewrite drags even an
explicit HTTPS URL back onto SSH, and a `-c url...insteadOf` counter-rewrite does **not** override
it — you have to bypass the global config outright:

```bash
GIT_CONFIG_GLOBAL=/dev/null git -C <repo> \
  -c credential.helper='!gh auth git-credential' \
  push https://github.com/<owner>/<repo>.git <branch>
```

The same form works for `fetch` and `ls-remote` (verified 2026-07-29 across four repos). **Never
report "push auth is down" as a blocker** — between `ssh-add` and this, there is always a way
through. Reading remote state through the API is still fine and cheaper:

```bash
gh api repos/<owner>/<repo>/branches/<default> --jq .commit.sha
```

Compare that SHA to `git rev-parse <default>` locally. Three outcomes:

- equal → in sync
- local SHA not on remote (`gh api repos/<o>/<r>/commits/<sha>` → 422) and remote SHA not in local
  log → **diverged**; inspect both sides before recommending anything
- local contains the remote SHA → unpushed commits

Check the **default branch per repo — it is not uniformly `main`.** Older repos often use
`master`. Get it from `git symbolic-ref refs/remotes/origin/HEAD` or the API's `default_branch`,
never by assumption.

When a repo diverges, say **which files** each side touched. A nightly bot commit to a generated
`health.json` against a local `TASKS.md` edit is a trivial `pull --rebase`; describing it as
"diverged" without that detail makes a 10-second fix sound like surgery.

## Phase 3 — Judge agent branches by their verdict, not their name

This phase applies when an agent runner (a nightly/overnight rig, a scheduled coding agent) writes
branches into these repos. Its branches (`overnight/*`, `agent/*`, or whatever prefix it uses) are
**not neglect** — they are the designed output of the runner. A well-built merge gate merges into
**local** main only when the reviewer verdict is `approve` with an empty merge checklist and the
project's verify command passes; pushing is never automated. A branch sitting unmerged is the system
working: it's the morning-coffee queue.

So don't guess. Read the runner's run report (wherever it writes one, e.g.
`<runner>/runs/<YYYY-MM-DD>/run.json`). Per task it should carry the task lead, the branch, the
reviewer verdict, a failed flag, and — critically — a top-level `warnings[]` that **states the exact
reason a merge gate declined**. That string is usually the whole answer ("approved but not
auto-merged — uncommitted local changes overlap the branch (…)").

Triage:

| Verdict | Meaning | Action |
|---|---|---|
| `approve`, not merged | gate was blocked by something mechanical | **the actionable merge** — clear the blocker, merge |
| `unreviewed` + `failed` | builder session errored before review | verify it yourself (Phase 4), then merge or discard |
| absent / parked | run died or was auto-parked | needs a human; don't recommend a merge |

Also list agent branches **already merged** into the default branch
(`git branch --merged <default>`) — those are pure cruft and safe to delete. A nightly runner
accumulates one per night.

## Phase 3.5 — Is the agent rig itself healthy?

Only if a runner exists (see Phase 3). The run report says what happened last night; these two say
whether tonight will happen at all. Both have silently wasted whole nights, so check them even when
every repo is clean.

**Weekly token ledger.** If the runner tracks budget (e.g. `run.json` carrying
`budget: {night, window, weekly}` and a `state/ledger.json`), note that a nightly cap is often
advisory: a first-task-per-project exemption lets a night overshoot it. The weekly ledger is the cap
that actually binds, and blowing it stands down *entire nights*:

```bash
node -e 'const l=JSON.parse(require("fs").readFileSync("state/ledger.json","utf8"));
const t=l.entries.filter(e=>Date.now()-e.at<7*864e5).reduce((s,e)=>s+e.tokens,0);
console.log(t.toLocaleString(),"of weekly cap");'
```

Report it as *nights of headroom left at last night's burn*, not a percentage — "0.8 nights left"
lands, "78% used" does not.

**Permission denials.** Grep the run report for `permission_denials`. A persona repeatedly denied
the same command is burning its turn budget rediscovering the denial and will die on `max_turns`
with an `unreviewed` verdict — which then reads as "the code is suspect" when the real fault is a
prompt/allowlist mismatch. Name the denied command in the report; that's the fix, not more turns.

## Phase 4 — Verify before you judge an unreviewed branch

`verdict: unreviewed` means *the reviewer never ran*, not *the code is bad*. The two are constantly
confused, and the cost of the confusion is throwing away good work. Build and test it:

- Rust → `cargo check && cargo test`
- Node → the project's own verify/test script

Report the result as evidence. "Compiles clean, 67/67 tests pass" turns a scary-looking
`wip: auto-saved uncommitted builder output (run cut short)` commit into an easy merge decision.
Note when the auto-save commit *message* is junk even though the diff is good — that's a squash/
reword on merge, not a reason to discard.

## Phase 5 — Classify uncommitted work

Three buckets, and the distinction is the point:

- **Commit it** — real authored change. Read the diff; if it carries a comment explaining a bug and
  a fix, it's finished work someone forgot to commit.
- **Ignore it** — build/dev artifacts. Check the repo's existing `.gitignore` philosophy before
  proposing a commit. A repo that explicitly keeps heavy binaries local means an 8 MB compiled
  plugin belongs in `.gitignore`, not in a commit.
- **Discard it** — a working-tree change that is *worse* than what's committed. Actually diff it.
  A dirty `status.json` whose last field is truncated mid-word is a clobbered write, and the fix
  is `git checkout --`, not a commit. This is exactly the class of thing that blocks a merge gate,
  so it shows up as a cause, not just a symptom.

## Phase 6 — Report

Lead with **blockers that gate everything else** — but only after proving they're real. "Push auth is
down" was reported as one on the first run and was actually a locked keychain: `ssh-add
~/.ssh/<key>` cleared it in ten seconds. Prove a credential is unusable *interactively* before
you call it a blocker, and prefer the Phase 2 incantation over declaring one at all.

Then an ordered action list, most-consequential first, each item naming the repo, the concrete
command(s), and the one-line why. Then a compact clean-repos table. Then any structural oddity worth
a decision but not urgent — e.g. a repo whose default branch is no longer its trunk (a feature branch
~59 commits ahead of a `master` that hasn't moved in months, with the live deploy building from the
feature branch). Flag those as "confirm, then fix", never auto-fix.

Close by asking which items to execute. Then execute only those.

## Phase 7 — Re-verify immediately before acting (this sweep races other sessions)

If the user keeps **one long-lived session per project**, then between reporting the action list and
executing it, a project session can do the work itself. Measured on the first run: in the eight
minutes after the report, other sessions merged an approved branch in one repo, committed a parser
fix in another, merged and groomed two more, and pruned five stale agent branches.

The first symptom is incoherence, not a clean error — a `git diff` on a file inspected moments ago
returns empty, and `git rev-parse master origin/master` dies with "Needed a single revision"
mid-command. So, per repo, immediately before touching it:

1. `git fetch` (or the API SHA check) and `git status --porcelain` again.
2. If the state moved, **check whether it moved to where you were going.** `git reflog --date=iso`
   gives timestamps and the operation, and `git diff origin/<default>...<default> --stat` confirms
   the landed content matches what you'd have merged. If it matches, the only work left is usually
   the push. Never force your original plan onto a repo that already got there.
3. Say plainly in the report that the change landed from outside the session. Claiming credit for
   another session's merge is the failure mode here.

## Known shapes worth checking every run

Keep a short per-workspace list of recurring shapes and check it each sweep. Patterns seen so far:

- **A repo that accumulates one merged agent branch per night**: prune them. If the same repo
  regenerates a data file **twice** (the agent locally *and* a GitHub Actions cron pushing its own
  refresh), they collide on every push. Resolve with `git checkout --theirs <file>` — CI's run is the
  canonical one — and don't mistake it for a real conflict.
- **A nightly that writes a status file into the repo** (e.g. `.dashboard/status.json` plus an
  untracked report): the tracked one routinely blocks its own merge gate. Check what is and isn't
  gitignored.
- **Two repos that share a hand-written parser shape** (a dashboard's roadmap parser and the runner's
  copy of it): a fix to one often sits uncommitted in the other.
- **Long-lived scratch worktrees** each ~40 commits ahead of the default branch: if the experiment
  they served is settled, ask whether to prune.

