pages: persimmons.app from persimmons/app root, phast.app off pages, extensionless HTML; decisions: phast + persimmons re-ported from canonical
This commit is contained in:
1 parent
679f74c9b0
commit
a60a9706cc
4 files changed
+13
-3
No files matched your search
@@ -39,3 +39,4 @@ Format: date · decision · why. Add at the bottom. Never edit an old entry; sup
|
||||
- 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.
|
||||
- 2026-10-14 · **The factory runs without the operator: Astra is on, and a steward merges engine PRs.** The operator delegated merging and admin to Claude and approved both explicitly. Astra (`astra.yml`, daily at 13:13 UTC, one Opus call) now labels its picks `queued` instead of `ready`, so they line up behind the existing queue and `promote_next` runs one card at a time (time `97b7667`). Engine PRs (anything under `.github/`) still never merge themselves: Time marks them needs-human, and the **Teahub steward**, a scheduled Claude session (cloud, every 6 hours at :47 past 01, 07, 13 and 19 UTC, automatic approval), reads each one, answers the reviewer, and merges it only if CI is green, it does what its card's DONE line says, it touches no workflows, secrets or deploy, and it doesn't weaken the spend governor or the protected-path rule. The steward also works open andons (never by raising a cap or changing `TIME_MODE`) and repairs a failed heartbeat. Its admin tokens follow the rule above: minted on the Fly box, deleted by id, table checked. Tonight time#54 and time#56 were merged that way and `TIME_ASTRA` was set (tokens 30, 31, 34, all deleted); tokens 32 and 33 belonged to another session and were already gone when checked. Why: the operator doesn't want to be the merge gate, and the engine still shouldn't change without someone reading the diff.
|
||||
- 2026-10-14 · **SKILLS.md is the skills contract**, the third root file beside `AGENTS.md` (how to move here) and `DESIGN.md` (what good looks like): routes (task → one to three skills, named by path), a ledger (upstream ref and tree, version, what each skill writes) and belts (⬜ admitted, 🟩 katas beat no-skill on a named seat, ⬛ grown from our traces, 💤 retired once a seat no longer needs it). Skills are never left to auto-trigger, and no model decides which skill to load. `.agents/skills/README.md` now points to it and AGENTS.md has the move. (The first skills README said Claude Code reads `.agents/skills/`; it reads `.claude/skills/` only.) Why: the Oct 14 skills research — unaided triggering fires about half the time, generic skills often add nothing on strong models, and the trust that holds comes from gates (evals, pinned refs, sandboxes), not prose. Time's seats are plain API calls, so routing has to live in a file every seat can read (#7).
|
||||
- 2026-10-14 · **phast and persimmons re-ported from their canonical repos; phast runs.** The port above copied `tea-mcp/src` snapshots from Oct 9, older than the canonical `matcha/phast` and `matcha/persims`. So phast.app came up in the old light design and persimmons.app on a holding page. Both canonical repos are now merged, with history, into `phast/app` and `persimmons/app`, and their trees are the canonical ones. **phast**: Fly app `phast-app` (1 machine, volume `phast_data`) serves phast.app, my.phast.app and `*.phast.app` from that one repo: the pages are its `index.html` and `my/index.html`, and the MCP at `my.phast.app/mcp` signs in with Teahub OAuth. The OAuth app is "Phast app", owned by `teahubxyz`, redirect `https://my.phast.app/auth/callback`; the client secret and JWT key exist only as Fly secrets and were never printed. `ALLOWED_GITEA_USERS=matcha,chai`. matcha's Phast app moved from hb-5. Closes phast/app#1. **persimmons**: pages serves persimmons.app from `persimmons/app`'s root (its `pages/page.conf` says `source=.`). The API isn't on prod, so the vault reads "Service unavailable". persimmons/app#1 covers the port, including whether a product backend may hold an admin-scoped token: it creates vault repos and stores PATs as Actions secrets. `phast/site`, `persimmons/site` and `persimmons/extension` are archived with pointers. Pages now serves extensionless HTML (`/signin`) as text/html. Why: the regression was a stale copy, so each product ships from the one repo its owner works in.
|
||||
+6
-1
@@ -8,4 +8,9 @@ The pages convention for orgs on prod: an org's public `site` repo is served at
|
||||
Adding a site: a line in `PAGES_SITES` + `fly deploy` (operator lane), `fly certs add <host>`, the `_acme-challenge`
|
||||
CNAME, then A/AAAA to the app's IPs (`fly ips list`), and a push webhook on the `site` repo.
|
||||
|
||||
Live: persimmons.app → `persimmons/site`, phast.app → `phast/site`.
|
||||
Live: persimmons.app → `persimmons/app` (repo root, as its `pages/page.conf` says), yolopreneur.lol and
|
||||
`*.yolopreneur.lol` → `teahubxyz/time#gh-pages/ventures` (`PAGES_WILDCARDS`). A site can be any public repo,
|
||||
`org/repo#branch/subdir`; it doesn't have to be called `site`. A product with a backend serves its own pages
|
||||
instead (phast.app is `phast-app`), so there is never a copy to drift.
|
||||
|
||||
Files without an extension that start with `<!doctype html` or `<html` are served as HTML (`/signin`).
|
||||
+1
-1
@@ -6,7 +6,7 @@ primary_region = "iad"
|
||||
|
||||
[env]
|
||||
GITEA_URL = "https://teahub.xyz"
|
||||
PAGES_SITES = '{"persimmons.app":"persimmons/site","phast.app":"phast/site","yolopreneur.lol":"teahubxyz/time#gh-pages/ventures"}'
|
||||
PAGES_SITES = '{"persimmons.app":"persimmons/app","yolopreneur.lol":"teahubxyz/time#gh-pages/ventures"}'
|
||||
PAGES_WILDCARDS = '{"yolopreneur.lol":"teahubxyz/time#gh-pages/ventures"}'
|
||||
PAGES_PULL_SECONDS = "300"
|
||||
|
||||
|
||||
+5
-1
@@ -152,8 +152,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_error(404)
|
||||
return
|
||||
data = target.read_bytes()
|
||||
ctype = TYPES.get(target.suffix.lower(), "application/octet-stream")
|
||||
lead = data[:64].lstrip().lower() # not `head`: that name is the HEAD-request flag
|
||||
if not target.suffix and (lead.startswith(b"<!doctype html") or lead.startswith(b"<html")):
|
||||
ctype = TYPES[".html"] # extensionless pages (e.g. /signin) are HTML, not downloads
|
||||
self.send_response(status)
|
||||
self.send_header("content-type", TYPES.get(target.suffix.lower(), "application/octet-stream"))
|
||||
self.send_header("content-type", ctype)
|
||||
self.send_header("content-length", str(len(data)))
|
||||
self.send_header("cache-control", "public, max-age=60")
|
||||
self.send_header("x-content-type-options", "nosniff")
|
||||
|
||||
Reference in new issue
Block a user