The owner, 2026-10-18/19: keep the GitHub copy and the Gitea home in step so cloud sessions (the $250 cloud-session credit, which only covers sessions that clone from GitHub) work on a current engine. More generally: several forges, some of them Gitea instances, staying synced on certain repos.
The rule: one home per repo, one writer per branch. True two-way sync of one branch is how work gets lost. Neither of Gitea's built-in mirror types fits:
A push mirror (git push --mirror) force-pushes every ref, so it would delete a cloud session's branch on GitHub at the next sync.
A pull mirror makes the home read-only.
What sync.py does (config in time/sync.json, run by sync.yml on every push to main, every 15 minutes, and on dispatch):
Out:
The home's out branches (default main) go to every copy, fast-forward only.
A copy with commits of its own on that branch is reported, once, in a needs-human issue. It is never overwritten.
The force dispatch input force-pushes once. That's for replacing the archived GitHub copy's old history on day one, if it isn't an ancestor.
In:
A copy's branches matching its import patterns (claude/*, the branches cloud sessions push) are the copy's to write.
They come to the home under the same name, with a PR whose body carries the commit messages, so a Closes #N in a commit adopts the card. Time's council reviews and lands them.
Landed work flows back out with main.
A branch already in main, or one whose PR was closed at the same commit, is left alone. A new push reopens the question.
Tokens: each one is a secret named in sync.json. It is never written in the file, and it is scrubbed from every error and message. A copy whose secret isn't set is skipped.
Gitea ↔ Gitea: the same script works. It runs on the home, so a pair the home can't reach (hb-5's local Gitea from prod) runs from the side that can reach both, with its own sync.json. Prod never depends on the Mac.
Before the GitHub copy goes live (the owner, on GitHub):
Unarchive github.com/teahubxyz/time.
Turn GitHub Actions off on it (Settings → Actions → General → Disable actions). Every workflow here (heartbeat, venture, astra, refresh, update…) would otherwise also run on GitHub.
Create a fine-grained token with Contents read/write on that repo, and add it here as the Actions secret GITHUB_MIRROR_TOKEN.
Until then the copy is skipped and the workflow is a no-op.
Tests: new test 83 runs against real local git repos (file:// home and copy):
main out, then up to date
a claude/* branch comes home with a PR (title and Closes #5 from the commit), while other branches stay on the copy
no duplicate PR
a rewritten copy branch is taken as it is
after landing, main goes out and no PR is made
a PR closed at the same commit stays closed, and a new commit reopens it
a diverged copy main is refused, flagged once as needs-human, then forced
a missing secret skips the copy; tokens are URL-quoted and scrubbed
no sync.json means nothing to sync
test_time.py passes locally on this branch's tree.
Includes a workflow file (sync.yml), so it's a human merge.
The owner, 2026-10-18/19: keep the GitHub copy and the Gitea home in step so cloud sessions (the $250 cloud-session credit, which only covers sessions that clone from GitHub) work on a current engine. More generally: several forges, some of them Gitea instances, staying synced on certain repos.
**The rule:** one home per repo, one writer per branch. True two-way sync of one branch is how work gets lost. Neither of Gitea's built-in mirror types fits:
- A push mirror (`git push --mirror`) force-pushes every ref, so it would delete a cloud session's branch on GitHub at the next sync.
- A pull mirror makes the home read-only.
**What `sync.py` does** (config in `time/sync.json`, run by `sync.yml` on every push to main, every 15 minutes, and on dispatch):
- **Out:**
- The home's `out` branches (default `main`) go to every copy, fast-forward only.
- A copy with commits of its own on that branch is reported, once, in a `needs-human` issue. It is never overwritten.
- The `force` dispatch input force-pushes once. That's for replacing the archived GitHub copy's old history on day one, if it isn't an ancestor.
- **In:**
- A copy's branches matching its `import` patterns (`claude/*`, the branches cloud sessions push) are the copy's to write.
- They come to the home under the same name, with a PR whose body carries the commit messages, so a `Closes #N` in a commit adopts the card. Time's council reviews and lands them.
- Landed work flows back out with `main`.
- A branch already in `main`, or one whose PR was closed at the same commit, is left alone. A new push reopens the question.
- **Tokens:** each one is a secret named in `sync.json`. It is never written in the file, and it is scrubbed from every error and message. A copy whose secret isn't set is skipped.
- **Gitea ↔ Gitea:** the same script works. It runs on the home, so a pair the home can't reach (hb-5's local Gitea from prod) runs from the side that can reach both, with its own `sync.json`. Prod never depends on the Mac.
**Before the GitHub copy goes live** (the owner, on GitHub):
1. Unarchive `github.com/teahubxyz/time`.
2. **Turn GitHub Actions off on it** (Settings → Actions → General → Disable actions). Every workflow here (heartbeat, venture, astra, refresh, update…) would otherwise also run on GitHub.
3. Create a fine-grained token with Contents read/write on that repo, and add it here as the Actions secret `GITHUB_MIRROR_TOKEN`.
Until then the copy is skipped and the workflow is a no-op.
**Tests:** new test 83 runs against real local git repos (`file://` home and copy):
- `main` out, then up to date
- a `claude/*` branch comes home with a PR (title and `Closes #5` from the commit), while other branches stay on the copy
- no duplicate PR
- a rewritten copy branch is taken as it is
- after landing, `main` goes out and no PR is made
- a PR closed at the same commit stays closed, and a new commit reopens it
- a diverged copy `main` is refused, flagged once as `needs-human`, then forced
- a missing secret skips the copy; tokens are URL-quoted and scrubbed
- no `sync.json` means nothing to sync
`test_time.py` passes locally on this branch's tree.
Includes a workflow file (`sync.yml`), so it's a human merge.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
One home per repo, one writer per branch: main goes out to every copy fast-forward only (a copy with commits of its
own is reported as a needs-human issue, never overwritten, unless forced once from the dispatch); a copy's branches
matching its import patterns (claude/* from cloud sessions on GitHub) come back to the home as PRs for the council.
The first copy is github.com/teahubxyz/time, skipped until GITHUB_MIRROR_TOKEN is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The owner, 2026-10-18/19: keep the GitHub copy and the Gitea home in step so cloud sessions (the $250 cloud-session credit, which only covers sessions that clone from GitHub) work on a current engine. More generally: several forges, some of them Gitea instances, staying synced on certain repos.
The rule: one home per repo, one writer per branch. True two-way sync of one branch is how work gets lost. Neither of Gitea's built-in mirror types fits:
git push --mirror) force-pushes every ref, so it would delete a cloud session's branch on GitHub at the next sync.What
sync.pydoes (config intime/sync.json, run bysync.ymlon every push to main, every 15 minutes, and on dispatch):outbranches (defaultmain) go to every copy, fast-forward only.needs-humanissue. It is never overwritten.forcedispatch input force-pushes once. That's for replacing the archived GitHub copy's old history on day one, if it isn't an ancestor.importpatterns (claude/*, the branches cloud sessions push) are the copy's to write.Closes #Nin a commit adopts the card. Time's council reviews and lands them.main.main, or one whose PR was closed at the same commit, is left alone. A new push reopens the question.sync.json. It is never written in the file, and it is scrubbed from every error and message. A copy whose secret isn't set is skipped.sync.json. Prod never depends on the Mac.Before the GitHub copy goes live (the owner, on GitHub):
github.com/teahubxyz/time.GITHUB_MIRROR_TOKEN.Until then the copy is skipped and the workflow is a no-op.
Tests: new test 83 runs against real local git repos (
file://home and copy):mainout, then up to dateclaude/*branch comes home with a PR (title andCloses #5from the commit), while other branches stay on the copymaingoes out and no PR is mademainis refused, flagged once asneeds-human, then forcedsync.jsonmeans nothing to synctest_time.pypasses locally on this branch's tree.Includes a workflow file (
sync.yml), so it's a human merge.🤖 Generated with Claude Code
https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD