Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fTCFiw6ohysUDcWoPsTys
40 lines
13 KiB
Markdown
40 lines
13 KiB
Markdown
# Decisions (append-only)
|
|
|
|
Format: date · decision · why. Add at the bottom. Never edit an old entry; supersede it with a new one.
|
|
|
|
## Standing rules
|
|
- Prod is the cloud instance at teahub.xyz. The Mac (hb-5) is the operator's playground and the archive of the old factory; prod never depends on it.
|
|
- No secrets on prod beyond: Gitea's own keys, the GitHub OAuth client, the MCP's OAuth client, and the seat keys listed in the ledger. Adding one is a decision.
|
|
- Every agent acts as itself (its own Gitea login). No service tokens.
|
|
- Port pieces from the old instance only when a card needs them, after reading them, one piece per commit, logged in docs/rebuild.md.
|
|
|
|
## Ledger
|
|
- 2026-10-14 · **Green Tea v1 is a fresh prod, not a migration.** Nothing copied from hb-5 (no issues, labels, secrets, tokens). Why: the old factory was scaffolding; the rebuild is the portability test, and chai gets a box with no dev cruft.
|
|
- 2026-10-14 · **GitHub-backed Gitea on Fly.** Gitea stays (ownership, a place for chai to be admin); GitHub is upstream for Time and the first login. Why: Time is GitHub-native and already works; Gitea is cheap to run and keeps the sovereignty option.
|
|
- 2026-10-14 · **Logins: GitHub now, nostr next.** Auto-registration from GitHub; no local sign-up. Why: one click for collaborators; nostr gives agents and anyone else a key-based identity (see well-known.online).
|
|
- 2026-10-14 · **Tea MCP is off the Mac.** New Green Tea MCP on Fly, OAuth against prod Gitea, every tool as the caller. Why: the MCP is the product surface; it should be reachable without the operator's machine and must never outrun the caller's permissions.
|
|
- 2026-10-14 · **Boring stays on the Mac.** It is the operator's admin lane, not a prod component. Why: the thing that can repair prod and the thing that runs prod are different boxes with different owners.
|
|
- 2026-10-14 · **Codex is not installed on prod.** Seats are API-driven (Claude API, Venice); Codex on demand as a Fly Sprite. Why: no resident credentials on the runner.
|
|
- 2026-10-14 · **Fly cost accepted** for prod (two small apps). Same category as Venice credit.
|
|
- 2026-10-14 · **Mac runners reversed.** Last week's move of runners onto hb-5 is undone on purpose (portability exercise).
|
|
- 2026-10-14 · **Venture loop on teahubxyz/time paused** until the audit's remaining items are judged safe (3 criticals + 3 highs fixed in time#45, #46).
|
|
- 2026-10-14 · **well-known.online is a separate product**, not a Teahub component; may later be prod's signup front door. No dependency either way.
|
|
- 2026-10-14 · **The Mac's Gitea stays, as a separate unit.** The operator keeps a Gitea on hb-5 to play with customisation. It is not connected to prod: no mirrors, no shared users, no shared tokens, no DNS under teahub.xyz pointing at it beyond the archive hostname matcha.teahub.xyz (which may go). Why: Teahub and the Mac are distinct units; the Mac's job is maths and experiments, prod's job is being prod.
|
|
- 2026-10-14 · **Graduated ventures are orgs on prod** (persimmons, phast, phly …), treated as independent clients of Teahub with their own repos and a pages convention. Not wholesale imports; each starts from what it actually is. Why: a venture with its own domain is a client, not a folder of the factory.
|
|
- 2026-10-14 · **notplanned + venture pages merge into Time's venture loop on GitHub**, not into prod Gitea. Why: Time already is the venture loop; two listings would drift.
|
|
- 2026-10-14 · **Scouting is plural.** Time's trending-repos scout stays the first source; notplanned's curated backlog is a second source into the same pitch round; more sources can be added the same way. notplanned is not "the" scout. Why: popularity and wanted-but-unbuilt are different signals, and the loop should be able to take several.
|
|
- 2026-10-14 · **The old Fly edge is down; its sites are dark on purpose.** `teahub-gitea` scaled to zero (app kept, no machines). Dark until ported: persimmons.app, phast.app, phly.cloud, yolopreneur.lol apex/www, supertestnet.lol, notplanned., maths., 🫖, matcha.teahub.xyz. Per-venture `<slug>.yolopreneur.lol` Workers are unaffected. The Mac is now reachable only over the tailnet; the last public wire into it is cut. Port order: persimmons, phast, yolopreneur apex. Why: the edge was the only thing coupling public DNS to the Mac, and nothing behind it had meaningful traffic.
|
|
- 2026-10-14 · **Time moves to prod. GitHub was the proof of concept.** Supersedes the "GitHub is upstream for Time" clause above and the "merge into Time on GitHub" entry: the factory runs on prod Gitea, in one place, with Gitea Actions. `teahubxyz/time` on GitHub becomes read-only once the port lands; no two-way issue sync, ever (Time's state is its issue comments, and a mirrored state machine is a conflict generator). Steps: Fly runner → Gitea backend behind Time's forge module → migrate `time` with issues once → flip workflows to `.gitea/workflows` → archive GitHub copy. Why: Time in one repo host and Teahub in another made no sense; everything on GitHub would defeat standing up Gitea at all.
|
|
- 2026-10-14 · **Runner on Fly: `green-tea-runner`** (act_runner 5.0.0-dind, iad, shared-cpu-2x/4GB, 10GB volume holding Docker's data root — Fly's rootfs is overlay so `/var/lib/docker` must live on the volume). Labels `ubuntu-latest`, `ubuntu-22.04` → `node:22-bookworm`. Registered instance-wide as `fly-iad-1`. Smoke: `teahubxyz/teahub` `.gitea/workflows/smoke.yml` green. Why: Actions on prod are a precondition for Time on prod.
|
|
- 2026-10-14 · **Time landed on prod** (`teahubxyz/time` native, migrated once with issues; port merged as time#47; GitHub copy archived with a pointer). Workflows stay in `.github/` (Gitea reads it). Trust = repo owner, `TIME_TRUSTED` variable, or collaborator, because the Actions token can't read permissions. `venture.py` is not ported yet (time#48); venture loop stays off. Why: see the entry above; one host, no sync.
|
|
- 2026-10-14 · **First product secret on prod: `VENICE_API_KEY`** on `teahubxyz/time` (repo Actions secret; the same key the old factory used, taken from its delivery on hb-5, never printed). With it: `TIME_WORKER=venice:qwen coder`, `TIME_REVIEW=venice:grok`, `TIME_MODE=live`, `TIME_AUTOMERGE=on` (merges only on a trusted Approve + green CI, never on protected paths). Why: seats are the product; the operator said the key lives with Boring's deliveries, and the factory is meant to run without hand-offs.
|
|
- 2026-10-14 · **Pages convention: `<org>/site` is served at the org's domain by `green-tea-pages`** (Fly, stdlib static host, one shallow clone per site, pull on a Gitea push webhook or every 5 min; code in `pages/`). Supersedes "Cloudflare Pages by default" in #2. No tokens anywhere: site repos are public and cloned over https. Why: the simplest thing that makes "a commit to site ships" true on prod itself, with nothing on the Mac and no new secret.
|
|
- 2026-10-14 · **persimmons and phast are ported as orgs, as they were.** `persimmons/app` (+ vendored `human_vault.py`), `persimmons/extension`, `phast/app` (+ `acp/`); `persimmons/site` and `phast/site` live at persimmons.app and phast.app (certs issued, DNS moved off the dead edge). Neither app runs yet: persimmons signs in with Gitea passwords, which prod does not have (needs Gitea OAuth, like the MCP); phast's production blockers (volume, owner auth) stand. persimmons.app says so on a holding page; phast.app serves its landing. Why: the domains were dark; the code is where its owner is; the runtime seams are cards, not blockers for the port.
|
|
- 2026-10-14 · **Skills are vendored into the spine at `.agents/skills/`**, not installed per seat. First set: `brag-slim` (latent-spaces/brag) and six of coreyhaines31/marketingskills (`product-marketing`, `launch`, `social`, `content-strategy`, `copywriting`, `marketing-loops`); `evals/` left out. Both MIT. Why: seats on prod are API-driven with no home directory to install into, so the repo checkout is the only place a skill reliably exists; the factory just got dev.to and Bluesky, and `marketing-loops` is the shape Time can schedule. More skills go in the same way, by copying the folder.
|
|
- 2026-10-14 · **One venture ledger, at yolopreneur.lol.** Time's ventures and notplanned's are one list (`ventures.json` + `backlog.json` on time's `gh-pages`, with a `source` field); `teahubxyz/notplanned` is on prod with its 4 issues; the index is hand-authored (neo-brutalist) and renders the JSON, so the generator writes data only. Supersedes the "notplanned merges into Time on GitHub" entry. Why: two listings drift, and the operator wanted one.
|
|
- 2026-10-14 · **Every venture is static, on Teahub pages, at `<slug>.yolopreneur.lol`. No Cloudflare.** `*.yolopreneur.lol` → `green-tea-pages` (wildcard cert); the apex `/<slug>/` redirects to the subdomain; a venture ships as `ventures/<slug>/` on `gh-pages` and nothing else is provisioned. The nine Workers (`time-<slug>`) and their custom domains were deleted with the Workers token on hb-5; `backend` is gone from `ventures.json`; `time.js` degrades without `/api/`. `cloudflare.py`/`worker.js` leave with the venture.py port (time#48). Kept for now: `time-aicharts` (the lab, no static twin — rebuilds on prod per #5) and `jev-prefab-decider` (not Time's). D1 databases could not be deleted with that token (no D1 scope); the Workers that used them are gone, so they cost nothing but still exist. Why: one format for all sixteen, and no Cloudflare spend.
|
|
- 2026-10-14 · **Second product secret on prod: `ANTHROPIC_API_KEY`, user-level on `teahubxyz`** (Actions secret shared by every repo teahubxyz owns; taken from Boring's delivery on hb-5, never printed; the Gitea token used to set it was minted on the Fly box with `write:user` and revoked within minutes). With it: Time gets Claude seats (`haiku`, `sonnet`, `opus`, `fable`) through the Claude API's OpenAI-compatible layer, `TIME_WORKER=claude:sonnet`, and a **spend governor**: a ledger in the repo (`time/ledger/<month>.jsonl`, one line per model call with tokens and USD), `TIME_DAILY_USD=5` and `TIME_MONTHLY_USD=100` as user-level variables; a daily breach opens an `andon` issue and waits for midnight, a monthly breach also flips `TIME_MODE` to `dry-run`. Venice stays wired but is no longer the worker. Why: the operator's Max plan outruns manual use and the factory should run on API credits unattended; Venice burned $20 of $75 before anyone noticed, so the cap is counted in dollars by code, not in launches, and no model decides whether to spend.
|
|
- 2026-10-14 · **Time is a Gitea user: `time`, with its own token.** Gitea 1.24 lets the Actions identity (`gitea-actions`) comment and label, and git-push with the task token, but never open or merge a pull request ("user must be a collaborator") or write through the contents/branches API (pre-receive denies it). So Time's writes go over `git push`, and the two PR calls (open, merge) go out as `time`: a bot user, collaborator (write) on `teahubxyz/time`, token scoped `write:repository,write:issue`, stored as the repo secret `TIME_TOKEN`, minted on the Fly box, never printed. Operator approved. Reading of the standing rule: Time is an agent and this is Time acting as itself, not a shared service token; nothing else uses `TIME_TOKEN`. Also today: the daily planner `astra.py`/`astra.yml` (one Opus call a day, queues ≤2 cards, writes to the "Astra log" issue; off until `TIME_ASTRA=on`), and the first Claude-seat card, time#49, is the one that found all three Gitea walls. Why: without an identity that can hold a PR the council has nothing to review and nothing ever lands.
|
|
- 2026-10-14 · **Correction: the temporary admin tokens were not revoked when this log said.** The `ANTHROPIC_API_KEY` entry says the token used to set the secret was "revoked within minutes"; it wasn't. `gitea admin user` has no command that deletes a token, and the call meant to do it failed without anyone seeing an error. Four short-lived `teahubxyz` tokens (for the user-level secret and variables, Time's repo variables, and the push that made PRs go out as `time`) stayed valid for 11 to 65 minutes, were never printed, and were deleted from the database by 20:23 UTC (`access_token` ids 22, 23, 24, 26). The only token on prod now is `time`'s own (`TIME_TOKEN`). From now on a temporary token is deleted with `sqlite3` on the Fly box, and the deletion is checked by listing the table before anyone writes "revoked". Why: this log is how the next agent knows which credentials exist.
|
|
- 2026-10-14 · **Every write Time makes, except comments and commit statuses, goes out as `time`.** Widens the entry above, which covered only opening and merging PRs. Gitea 1.24 also refuses the Actions identity label edits (removing a label is a 500); that crashed `promote_next` after time#50 merged and stalled the queue. `gh()` now sends every non-GET call with `TIME_TOKEN` except comments and statuses, which stay with `gitea-actions` because Time's beats are that author's comments. The `time` user never counts as trusted, mention replies ignore it, and `astra.yml` gets the token too (time `c2055e8`). Three more temporary admin tokens were used this evening, to read job logs and push the fix; each was deleted by id and the table checked (ids 27, 28, 29). Also found: Gitea's label events carry no label name, so labelling a card `ready` doesn't wake the heartbeat; promoted cards wait for the hourly tick. Why: one identity that can actually do the factory's writes, and the Actions identity kept for the two things it reliably does.
|