Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/collect.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: collect

on:
schedule:
- cron: "23 5 * * *"
workflow_dispatch:
# Bob's host sends private monthly aggregates (brain_commits, sessions) here;
# see collect_private.py --emit and scripts/stats/bob-stats-sync.sh in the brain repo.
repository_dispatch:
types: [private-aggregates]

permissions:
contents: write

concurrency:
group: collect
cancel-in-progress: false

jobs:
collect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt
- run: pytest -q
- name: Save private aggregates payload
if: github.event_name == 'repository_dispatch'
env:
PAYLOAD: ${{ toJSON(github.event.client_payload) }}
run: printf '%s' "$PAYLOAD" > "$RUNNER_TEMP/private.json"
- name: Collect, render, commit if changed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT: ${{ github.event_name }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
for attempt in 1 2 3; do
git fetch -q origin master
git reset -q --hard origin/master
rc=0
if [ "$EVENT" = "repository_dispatch" ]; then
python collect_private.py --apply "$RUNNER_TEMP/private.json" || rc=$?
msg="chore(data): update private aggregates"
else
python collect_public.py || rc=$?
msg="chore(data): update public metrics"
fi
# rc=3: a frozen row would have changed; drift is logged, commit it, then fail.
if [ "$rc" -ne 0 ] && [ "$rc" -ne 3 ]; then exit "$rc"; fi
python render.py
if [ -z "$(git status --porcelain)" ]; then
echo "No changes"
exit "$rc"
fi
git add data charts LIFETIME.md README.md
git commit -q -m "$msg"
if git push -q origin HEAD:master; then
exit "$rc"
fi
echo "Push raced another run; retrying"
sleep $((attempt * 15))
done
exit 1
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: test

on:
push:
branches: [master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt
- run: pytest -q
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
.pytest_cache/
.venv/
*.csv.tmp
68 changes: 68 additions & 0 deletions LIFETIME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Bob's lifetime numbers

> **Quote these; don't re-estimate.** Generated by `render.py` from [`data/ledger.csv`](data/ledger.csv), a month-frozen ledger. Do not edit by hand.

How to cite:

> As of 2026-09-14: 4,038 merged public PRs (https://github.com/TimeToBuildBob/stats)

## Lifetime totals

| Metric | Lifetime | Frozen total (through) | Current month (partial) | As of | Scope | Since |
|---|---:|---:|---:|---|---|---|
| Merged public PRs (`prs_merged_public`) | **4,038** | 3,774 (2026-08) | 264 (2026-09) | 2026-09-14 | public | 2024-11 |
| Opened public PRs (`prs_opened_public`) | **4,386** | 4,100 (2026-08) | 286 (2026-09) | 2026-09-14 | public | 2024-11 |
| Opened public issues (`issues_opened_public`) | **349** | 328 (2026-08) | 21 (2026-09) | 2026-09-14 | public | 2024-11 |
| Public commits (default branches) (`commits_public_default_branch`) | **8,223** | 7,843 (2026-08) | 380 (2026-09) | 2026-09-14 | public | 2024-11 |
| Brain repo commits (private) (`brain_commits`) | **89,128** | 80,879 (2026-08) | 8,249 (2026-09) | 2026-09-14 | private | 2025-08 |
| Agent sessions (private, approximate) (`sessions`) | **18,708** | 16,571 (2026-08) | 2,137 (2026-09) | 2026-09-14 | private | 2026-06 |

## Definitions

- **`prs_merged_public`** (public, since 2024-11): PRs authored by TimeToBuildBob in public repos, by merge month (GitHub issue search).
- **`prs_opened_public`** (public, since 2024-11): PRs authored by TimeToBuildBob in public repos, by creation month (any final state).
- **`issues_opened_public`** (public, since 2024-11): Issues authored by TimeToBuildBob in public repos, by creation month.
- **`commits_public_default_branch`** (public, since 2024-11): Commits authored by TimeToBuildBob in public repos by committer date (GitHub commit search). Commit search indexes default branches only; squash merges count as one commit.
- **`brain_commits`** (private, since 2025-08): Commits on master of Bob's private brain repo whose author name is 'Bob' (bob@superuserlabs.org, plus a few early/misconfigured Bob identities), by committer date. Starts 2025-08: earlier Bob work was committed under Erik's identity and cannot be separated. Excludes Erik, other agents, and test-fixture identities (Test <test@test.com> etc.).
- **`sessions`** (private, since 2026-06): Bob agent sessions that ran to a result record (state/sessions/*.result.json): agent_id 'bob-*', status 'completed' with reason clean_exit/nonzero_exit/timeout and duration >= 60s, by completed_at month, deduplicated by session_id. Excludes lock/coordination skips and sub-minute startup failures (never did work), watchdog stubs, and '<synthetic>'/test/mock/unknown models. Starts 2026-06: result records begin 2026-05-20, so 2026-05 is incomplete and omitted.

The exact query or command for every value is stored per row in the ledger's `query` column.

## Caveats

- **Month-frozen.** A month is recomputed daily until 3 days after it ends, then frozen and never rewritten. Lifetime = sum of all months (frozen months + the open current month).
- **Public and private are separate scopes.** Never add `brain_commits` to `commits_public_default_branch`, and never add private numbers to public ones.
- **Commit search sees default branches only.** Squash merges collapse a PR's commits into one, so `commits_public_default_branch` undercounts commits actually written.
- **Search counts reflect visibility at compute time.** A repo that goes private or is deleted later does not change frozen months.
- **Private metrics are lower bounds.** `brain_commits` starts 2025-08 (earlier Bob work was committed under Erik's identity); `sessions` starts 2026-06 (no reliable records before) and is approximate.
- Months and date ranges are UTC. "As of" = the date the metric's value last changed.

## Monthly values

`*` = open month (still recomputed daily); all other values are frozen.

| Month | `prs_merged_public` | `prs_opened_public` | `issues_opened_public` | `commits_public_default_branch` | `brain_commits` | `sessions` |
|---|---:|---:|---:|---:|---:|---:|
| 2026-09 | 264* | 286* | 21* | 380* | 8,249* | 2,137* |
| 2026-08 | 472 | 535 | 48 | 675 | 15,663 | 4,754 |
| 2026-07 | 517 | 553 | 51 | 798 | 14,056 | 5,011 |
| 2026-06 | 390 | 411 | 14 | 912 | 16,431 | 6,806 |
| 2026-05 | 559 | 605 | 49 | 1,379 | 15,195 | |
| 2026-04 | 464 | 484 | 30 | 1,024 | 5,479 | |
| 2026-03 | 555 | 575 | 42 | 1,108 | 4,788 | |
| 2026-02 | 378 | 433 | 20 | 811 | 2,046 | |
| 2026-01 | 141 | 155 | 27 | 222 | 1,748 | |
| 2025-12 | 136 | 153 | 28 | 269 | 1,532 | |
| 2025-11 | 73 | 83 | 3 | 309 | 2,097 | |
| 2025-10 | 81 | 103 | 15 | 289 | 1,829 | |
| 2025-09 | 2 | 3 | 0 | 9 | 14 | |
| 2025-08 | 0 | 0 | 0 | 0 | 1 | |
| 2025-07 | 3 | 3 | 0 | 13 | | |
| 2025-06 | 0 | 1 | 0 | 2 | | |
| 2025-05 | 0 | 0 | 0 | 2 | | |
| 2025-04 | 0 | 0 | 1 | 0 | | |
| 2025-03 | 1 | 1 | 0 | 5 | | |
| 2025-02 | 0 | 0 | 0 | 0 | | |
| 2025-01 | 2 | 2 | 0 | 0 | | |
| 2024-12 | 0 | 0 | 0 | 4 | | |
| 2024-11 | 0 | 0 | 0 | 12 | | |
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Bob's lifetime stats

Lifetime numbers for [Bob](https://github.com/TimeToBuildBob) (TimeToBuildBob), an autonomous AI agent: PRs, issues, commits and sessions.

**This repo is the source of truth for Bob's lifetime numbers. Quote them; don't re-estimate them.**

Estimates like "how many PRs has Bob merged?" used to come out differently depending on when and how they were computed. This repo stores them in a **month-frozen ledger**, so a quoted number stays consistent with every earlier and later quote.

## Latest numbers

<!-- stats:start -->
| Metric | Lifetime | Current month (partial) | As of | Scope |
|---|---:|---:|---|---|
| Merged public PRs (`prs_merged_public`) | **4,038** | 264 (2026-09) | 2026-09-14 | public |
| Opened public PRs (`prs_opened_public`) | **4,386** | 286 (2026-09) | 2026-09-14 | public |
| Opened public issues (`issues_opened_public`) | **349** | 21 (2026-09) | 2026-09-14 | public |
| Public commits (default branches) (`commits_public_default_branch`) | **8,223** | 380 (2026-09) | 2026-09-14 | public |
| Brain repo commits (private) (`brain_commits`) | **89,128** | 8,249 (2026-09) | 2026-09-14 | private |
| Agent sessions (private, approximate) (`sessions`) | **18,708** | 2,137 (2026-09) | 2026-09-14 | private |

Definitions and caveats: [LIFETIME.md](LIFETIME.md). Public and private rows are never summed.
<!-- stats:end -->

![Merged public PRs per month](charts/prs_merged_monthly.svg)
![Lifetime public PRs](charts/lifetime_prs.svg)
![Commits per month](charts/commits.svg)

## How to cite

Always give the date, and link here:

> As of YYYY-MM-DD: N merged public PRs (https://github.com/TimeToBuildBob/stats)

Take `N` and the date from [LIFETIME.md](LIFETIME.md) or [`data/lifetime.json`](data/lifetime.json). If you need a number that isn't here, add a metric instead of estimating it ad hoc.

## Method: a month-frozen ledger

[`data/ledger.csv`](data/ledger.csv) holds one row per metric and month:

```
metric,month,value,frozen,source,query,computed_at
```

- **Recompute only while open.** A month's value is recomputed daily until `month_end + 3 days` (UTC). After that the row is `frozen=true`.
- **Frozen rows never change.** If a recompute of a frozen month gives a different value, the collector refuses to write it. It logs the difference to `data/drift.log` and exits non-zero.
- **Definition changes get a new name.** For example, `prs_merged_public_v2`. History is never rewritten.
- **Lifetime = sum over months.** It is the frozen months plus the current open month.
- **Every row records its exact query**, so any value can be re-derived.

These rules are enforced in [`ledger.py`](ledger.py) and covered by [`tests/`](tests/).

## Sources

| Collector | Where it runs | Metrics |
|---|---|---|
| [`collect_public.py`](collect_public.py) | GitHub Actions, [daily](.github/workflows/collect.yml); uses `GITHUB_TOKEN` only | `prs_merged_public`, `prs_opened_public`, `issues_opened_public`, `commits_public_default_branch` |
| [`collect_private.py`](collect_private.py) | Computed on Bob's host by a daily systemd timer, which reads the private brain repo and sends the totals as a `repository_dispatch` event. Applied by the same workflow | `brain_commits`, `sessions` |

Every public query includes `is:public`. Private collection sends monthly integer totals only: no repo names, titles or content leave the host. The workflow validates each payload and refuses anything but private integer rows. Only the Actions workflow writes the ledger, and each collector writes only its own metrics.

[`render.py`](render.py) regenerates the charts, [LIFETIME.md](LIFETIME.md), [`data/lifetime.json`](data/lifetime.json) (for websites and profile READMEs) and the table above. The output is deterministic, so a commit only happens when a number changes.

## Caveats

- **Public and private are never summed.** `brain_commits` (a private repo) and `commits_public_default_branch` measure different things.
- **Commit search sees default branches only.** Squash merges collapse a PR into one commit, so public commits undercount the commits Bob actually wrote.
- **`sessions` is approximate and a lower bound.** It starts at 2026-06, the first month with reliable session records. `brain_commits` starts at 2025-08, when Bob began committing under his own identity.
- **Frozen months are fixed at freeze time.** A repo made private or deleted later doesn't change them.

The exact per-metric definitions are in [LIFETIME.md](LIFETIME.md).

## Related

- [gptme/stats](https://github.com/gptme/stats): project stats for gptme (stars, downloads, releases). That is a separate ledger, and its numbers are never mixed with Bob's.
- [Bob's timeline](https://timetobuildbob.github.io/timeline/): month-by-month highlights. The source is [`_data/timeline.yml`](https://github.com/TimeToBuildBob/TimeToBuildBob.github.io/blob/master/_data/timeline.yml).
- [Bob's profile README](https://github.com/TimeToBuildBob)
Loading
Loading