# Alojamiento: la organización `PBSWORK` como fuente canónica ## Qué ocurrió La cuenta de GitHub `PBSWORK-ORG` quedó marcada (*flagged*) por el sistema antispam de GitHub. El síntoma es un **fallo silencioso**: la cuenta y todos sus repositorios pasan a devolver `404` a usuarios no autenticados, sin correo, sin aviso y sin entrada en el panel. Lo insidioso es que **el dueño es la única persona que no ve el 404**. Con sesión iniciada todo funciona con normalidad, así que el repositorio parece público mientras está invisible para el mundo. Un enlace compartido en una respuesta, un paper o un LLM que lo citó deja de resolver, y no hay forma de enterarse salvo comprobándolo a propósito. ## La decisión: mover el repositorio a la organización La hipótesis de partida era que el flag afecta a la **cuenta** y no al contenido, así que bastaba con sacar el repositorio de la cuenta marcada y ponerlo bajo la organización `PBSWORK` (id `198443207`, creada en 2025). Se verificó con `PBSWORK/MESH_SILOS`, que respondía `200` sin credenciales en ese momento. Movimiento ejecutado (resultado real, no el previsto): | Antes | Después | | --- | --- | | `github.com/PBSWORK-ORG/HECATE` (404 público) | `github.com/PBSWORK/HECATE` (404 público) | | Cuenta personal marcada | Organización también oculta a invitados | | Remoto `origin` con el owner antiguo | `git@github.com:PBSWORK/HECATE.git` | El nombre del repositorio no cambia, así que la URL final es `github.com/PBSWORK/HECATE`. Al no aceptar GitHub la transferencia, el contenido se replicó con `git push`: se conservan historial, etiquetas y releases, pero **no** los metadatos del panel (issues, PRs, estrellas) del repositorio original. **Importante: para trabajar, el flag es irrelevante.** El titular, con sesión iniciada, crea repositorios, hace `push`, etiqueta, abre issues y ve todo con normalidad. La marca solo afecta a la visibilidad **anónima**, es decir, a cualquiera que abra la URL sin sesión. Por eso el repositorio es completamente funcional como entorno de desarrollo; lo que queda pendiente es que un desconocido pueda verlo. ## Por qué no Codeberg Una revisión anterior de este documento proponía Codeberg como fuente canónica. **No se usó nunca**: no existe cuenta ni remoto allí, y el titular solo tiene cuenta de GitHub. La propuesta se retiró de todos los metadatos (`CITATION.cff`, `.well-known/ai.txt`, `llms.txt`) para no publicar enlaces que no resuelven. Si algún día se añade un espejo, se decidirá con una cuenta real y se documentará aquí antes de tocar los enlaces. ## Estado actual (honesto) | Paso | Estado | | --- | --- | | 1. Todo el trabajo publicado en el remoto antiguo antes de mover nada | **hecho** (`git push origin main --tags`) | | 2. Transferencia `PBSWORK-ORG/HECATE` → `PBSWORK/HECATE` desde los ajustes | **rechazada por GitHub**: `422 · Repository transfers not available for this account`. Una cuenta marcada no puede transferir. | | 3. Alternativa ejecutada: repositorio vacío creado en la organización | **hecho** (`PBSWORK/HECATE`, público) | | 4. Historial y etiquetas empujados a la organización | **hecho**: `24` commits y `v0.3.0`–`v0.6.0` verificados con `git ls-remote` | | 5. Remoto `origin` apuntando a la organización | **hecho** (`git@github.com:PBSWORK/HECATE.git`) | | 6. Enlaces públicos a `github.com/PBSWORK/HECATE` | **hecho** en `site/`, `pyproject.toml`, `Cargo.toml`, `CITATION.cff`, `.well-known/ai.txt` y `llms.txt` | | 7. Comprobación **sin credenciales** de que la URL nueva resuelve | **devuelve `404`**, que es el comportamiento esperado mientras la cuenta esté marcada | | 8. Apelación del flag en GitHub Support | **pendiente, lo abre el titular de la cuenta** | ### Estado del flag en `1.1.0` (2026-10-23) **Sigue marcada.** No se ha apelado desde el agente y no se debe hacer: la apelación la abre el titular de la cuenta, con su identidad, desde [GitHub Support](https://support.github.com/contact). Un agente que enviara esa solicitud estaría hablando en nombre de una persona ante el sistema antispam de GitHub. Consecuencia práctica, sin cambios: el desarrollo funciona entero con sesión iniciada; lo que no funciona es la audiencia sin sesión. Por eso `docs/release-1.0.0.md` y `docs/benchmarks/final.md` existen como documentos del repositorio y no sólo como páginas web: si nadie anónimo puede ver el sitio, los números tienen que poder leerse en el árbol. Los ocho enlaces canónicos siguen apuntando a `github.com/PBSWORK/HECATE`. No se ha cambiado ninguno a la organización antigua. ### Por qué el `404` anónimo no bloquea el trabajo Mover el repositorio a la organización **no** ha cambiado la visibilidad anónima. Comprobado sin credenciales, después del traslado: | Consulta anónima | Resultado | | --- | --- | | `api.github.com/orgs/pbswork/repos` | `200` y lista `PBSWORK/HECATE` y `PBSWORK/MESH_SILOS` como `public` | | `api.github.com/repos/PBSWORK/HECATE` | `404` | | `github.com/PBSWORK/HECATE` | `404` | | `github.com/PBSWORK` (página de la organización) | `404` | | `github.com/PBSWORK/MESH_SILOS` | `404` — respondía `200` minutos antes del traslado | | `github.com/rust-lang/rust` (control) | `200` | | Proxy externo sobre `github.com/PBSWORK/HECATE` | `404`, confirmado desde otra red | La API de la organización sigue describiendo los repositorios como públicos mientras la web los oculta a los visitantes sin sesión. La consecuencia práctica es que el flag viaja con la identidad: cualquier namespace del titular, incluida la organización, queda oculto a guests. Crear el repositorio nuevo no lo empeoró de forma demostrable, pero tampoco lo arregló, y la hipótesis de que el flag era solo de la cuenta personal no se sostuvo. **Con sesión iniciada todo funciona**: crear repositorios, `push`, etiquetas, issues y la web del proyecto. El flag no limita el desarrollo, solo la audiencia. Ese es el motivo por el que este detalle se documenta y no se trata como un bloqueante. Lo que **sí** está resuelto, y no es poco: el contenido ya no vive en un único sitio. El historial completo, las cuatro etiquetas y el árbol de trabajo están replicados en un namespace distinto del marcado. La apelación deja de ser urgente para no perder trabajo. Ningún script de este repositorio finge que la migración está hecha. `just links` reporta como no resolubles las URL que no responden, que es la verdad. Detalle importante de orden: publicar **antes** de mover. Si se traslada con commits locales sin publicar, se pierden si algo sale mal; publicados, el traslado no puede llevarse por delante nada que no esté ya replicado. ## Cómo se hizo el traslado 1. `git push origin main --tags` contra el remoto antiguo, para que nada quedara solo en local. 2. **Settings → Danger Zone → Transfer ownership** con destino `PBSWORK`. GitHub lo rechazó con `422`: una cuenta marcada no puede transferir. 3. Alternativa: crear `PBSWORK/HECATE` vacío (público, sin README ni licencia) desde la organización. 4. `git remote set-url origin git@github.com:PBSWORK/HECATE.git` 5. `git push -u origin main` y `git push origin --tags`. Consecuencia de la alternativa: se conserva el 100 % del historial, pero no los metadatos del panel (issues, PRs, estrellas) del repositorio original. Y, como queda documentado arriba, la visibilidad anónima sigue dependiendo de la apelación. ## Si la apelación no prospera Un alojamiento cuyo modo de fallo es invisible no puede ser la única fuente pública. Las opciones, en orden de coste: 1. **Apelar** (`Account Questions` → `Appeal a flagged account`). Es lo primero y lo único que devuelve la visibilidad anónima a la normalidad. 2. **Espejo en otra forja** con una cuenta distinta de la marcada. Requiere crear esa cuenta; hasta entonces no se anunciará ningún enlace, porque un espejo que no existe es peor que ninguno. 3. **Publicación de artefactos por otra vía**: el sitio en `hecateforge.com` (Cloudflare, no depende de GitHub) y los binarios como artefactos de *release* alojados fuera del repositorio. ### La tercera vía ya está en marcha El sitio se publica en Cloudflare Pages y ya sirve `https://hecateforge.com` sin pasar por GitHub: es la única pieza del proyecto que hoy es visible sin sesión y sin depender del flag. El manual de operación —despliegue, dominio, cabeceras y lo que queda pendiente— está en `docs/hosting-cloudflare.md`. Consecuencia práctica: mientras el flag dure, la web es el canal público y el repositorio es documentación de trabajo, no la cara del proyecto. ## Publicar a partir de ahora El remoto ya es la organización, así que cada release se publica una sola vez: ```bash just audit # la verificación de siempre git tag -a v0.8.0 -m "v0.8.0 — ..." git push origin main --tags ``` Comprobar con `git remote -v` que `origin` sigue en `PBSWORK` antes de etiquetar. Si alguien restaura el remoto antiguo por error, las etiquetas acabarían en el namespace marcado. ### Lista de comprobación de publicación Se recorre entera antes de cada `push` que publique algo, y se copia el resultado en el `CHANGELOG`. No es burocracia: el modo de fallo de este alojamiento es invisible, así que lo único que lo detecta es comprobarlo a propósito. | # | Comprobación | Comando | Criterio | | --- | --- | --- | --- | | 1 | Árbol limpio, sin cambios ajenos mezclados | `git status --porcelain` | vacío | | 2 | El remoto apunta a la organización | `git remote -v` | `PBSWORK/HECATE`, **no** `PBSWORK-ORG` | | 3 | Los commits que se publican son los previstos | `git log --oneline origin/main..HEAD` | sólo los de esta publicación | | 4 | La suite pasa en local | `just check` | verde | | 5 | La auditoría de repositorio pasa | `just audit` | verde | | 6 | Los fixtures de paridad están al día | `just fixture` | `--check` sin diferencias | | 7 | La versión es la misma en los cinco sitios | `just versions` | `Cargo.toml`, `pyproject.toml`, `CITATION.cff`, `llms.txt`, `package.json` | | 8 | La etiqueta es anotada y describe el cambio | `git tag -a vX.Y.Z -m "..."` | `git cat-file -t` responde `tag` | | 9 | Publicar **antes** de mover nada | `git push origin main --tags` | `git ls-remote --tags origin` lista la nueva | | 10 | Los enlaces públicos resuelven **sin sesión** | `just links` | `2xx`/`3xx` en todos | | 11 | El nombre del repositorio no depende de la persona que empuja | `curl -s -o /dev/null -w '%{http_code}' https://github.com/PBSWORK/HECATE` | `2xx` cuando el flag esté resuelto; **`404` mientras no lo esté, y se reporta como tal** | Los pasos 1–7 y 11 son los que evitan los cuatro fallos que ya han ocurrido en este proyecto: mezclar trabajo ajeno en un commit de release, etiquetar contra el remoto equivocado, publicar con los fixtures desactualizados y una versión distinta en cada metadato. ### El nombre canónico no se negocia **`PBSWORK/HECATE` es el repositorio, y no se cambia.** El remoto `origin` de todos los clones apunta ahí (`git@github.com:PBSWORK/HECATE.git`), los cinco metadatos lo declaran (`Cargo.toml`, `pyproject.toml`, `CITATION.cff`, `.well-known/ai.txt`, `llms.txt`) y el sitio enlaza ahí. Cualquier herramienta o panel que muestre otro propietario describe un estado transitorio del cliente, no una decisión del proyecto: la respuesta es comprobar con `git remote -v`, no reescribir URLs. Mover los enlaces a otro namespace para que una comprobación salga en verde sería el peor cambio posible: rompería el remoto, la citación y los enlaces del sitio a la vez para satisfacer a una herramienta. Si algún día hay que cambiar de propietario, se hace como una migración —documentada aquí, con el contenido replicado y verificado **antes** de tocar un solo enlace— y no como un ajuste de texto. ### Nota de estado (2026-10-23) La comprobación anónima del paso 11 sigue devolviendo **`404`** en `github.com/PBSWORK/HECATE` y en `github.com/PBSWORK/HECATE/releases`, mientras la API de la organización lista el repositorio como público. Es el comportamiento descrito arriba: el flag oculta el namespace a los visitantes sin sesión aunque el contenido sea público. Se reporta como está y no se "arregla" cambiando URLs. ### El duplicado se retiró (2026-10-28) `github.com/PBSWORK-ORG/HECATE` **ya no existe**: el titular lo borró el 2026-10-28. La comprobación de que no se perdía nada se hizo antes de retirarlo, y se repite aquí porque es el motivo por el que el borrado fue seguro: | Comprobación | Resultado tras el borrado | | --- | --- | | `git ls-remote https://github.com/PBSWORK-ORG/HECATE.git` | falla: el repositorio ya no existe | | `git ls-remote origin refs/heads/main` | `67a3668`, la release actual | | `git rev-parse HEAD` en este clon | `67a3668`, el mismo commit | | `git merge-base --is-ancestor db0bbe7 HEAD` | **sí** — el último commit del duplicado estaba contenido | | etiquetas `v*` en `PBSWORK/HECATE` | 27 publicadas; 28 en local, porque `v1.9.0-final` es local a propósito | El duplicado tenía una sola rama `main` en `db0bbe7` —«mueve el owner canónico a la organización»— y **ninguna etiqueta `v2.x`**: era el original que se replicó, no una copia que recibiera trabajo. Por eso `git merge-base --is-ancestor db0bbe7 HEAD` respondía que sí, y borrarlo no perdió ni un commit. `refs/heads/main` en `PBSWORK/HECATE` siguió en la release actual durante todo el proceso. La vía **reversible** que documentaba esta sección —*Archive this repository* en lugar de *Delete*— ya no aplica: el repositorio está eliminado. Si alguna vez hay que volver al estado anterior, el contenido está en `PBSWORK/HECATE`, que es el canónico y el único remoto. Una consecuencia que conviene tener presente: `git ls-remote` contra el namespace borrado ahora **falla**. La asimetría que se documentaba aquí —el protocolo git servía el namespace marcado aunque la web diera `404`— sólo era válida mientras el repositorio existía, así que ya no sirve como forma de auditar el namespace de la cuenta personal sin credenciales. ### Sobre Codeberg No queda **ninguna URL** a Codeberg: ni en el código, ni en los metadatos, ni en el sitio. La comprobación es `git grep -rn 'codeberg\.'`, que no devuelve nada —`git grep -ri codeberg` sí devuelve estas líneas y las del `CHANGELOG`, que son la explicación de por qué no se usó, no un enlace—. Las menciones originales se retiraron en la fase 7. La regla vigente es: **GitHub es el único remoto, y el nombre canónico es `github.com/PBSWORK/HECATE`** hasta que la comprobación del paso 11 diga otra cosa. ## Auto-auditoría mensual El fallo es silencioso, así que hay que comprobarlo a propósito. **Desde una sesión no autenticada** —ventana privada, o `curl` sin credenciales— los enlaces deben resolver: ```bash just links # o a mano: curl -s -o /dev/null -w '%{http_code}\n' https://github.com/PBSWORK/HECATE curl -s -o /dev/null -w '%{http_code}\n' https://hecateforge.com ``` `scripts/check_hosting_links.py` automatiza esto: recorre los enlaces públicos declarados en `llms.txt`, `ai.txt`, `CITATION.cff` y `README.md`, los pide **sin credenciales** y falla si alguno no responde `2xx`/`3xx`. Detecta los tres casos que duelen: - una cuenta o un repositorio marcado (404 en el canónico), - un enlace a un destino que todavía no existe (404 en una migración a medias), - un enlace interno que apunta a un fichero que ya no existe. No está en `just check` ni en `scripts/verify.sh` porque **necesita red**: la regla del proyecto es que la verificación obligatoria funcione sin conexión. Es una tarea manual mensual, y la primera del mes. ## Lección para el diseño Un alojamiento con fallo silencioso obliga a tener un plan que no dependa de él: 1. La fuente canónica no puede ser un servicio cuyo modo de fallo sea invisible. 2. Un namespace marcado se abandona, no se espera: el contenido sano se muda a uno que no lo esté. 3. Todo enlace público debe ser comprobable **sin** la sesión del dueño; si no, no hay auditoría posible. 4. El proyecto debe seguir siendo verificable y clonable sin su forja: de ahí que la verificación sea local y que los metadatos (`CITATION.cff`, `llms.txt`) viajen en el propio repositorio y no en un panel externo.