Objective
Move dev-host provisioning out of clankerlabs/clankerengineer into
clankerlabs/CLANKERNET, and make CLANKERNET a standalone product — “Deploy Your Own
Clanking Machine”: any repo commits a factory.yml and gets Traefik-fronted developer
containers on a host in the customer’s OWN cloud accounts (BYOC from day one). Move the
GitFWD Expo client (whitehatgg/GitFWD/clients/expo) into CLANKERNET as the product’s
app. The unmerged branch clankerengineer:feat/gitfwd-coordination (2 commits,
2026-09-14: vendored GitFWD skill/CLI/MCP, deploy/gitfwd/ gateway + auth-bridge,
dev-container GFP/1 self-enrolment, gitfwd-factory.test.ts) is the seed of the gateway
service and is EXTRACTED into CLANKERNET, never merged into clankerengineer.
User decisions taken during planning:
Reuse the live dev-host’s stateSUPERSEDED 2026-09-16 (D23): full teardown of CE’s dev-host (bucket and Redis included), fresh host from CN, engineers re-provision; then delete all dev-host provisioning code from clankerengineer.- Domain split: the Clanker product (landing, app, API, staging) moves to
clanker.engineer; CLANKERNET ownsclanker.netand thenet.clankerapp identity. Engineers stay ondev-{user}.clanker.net— dev containers are factory-provided hosts on the factory’s zone, nobody moves. - Apps: clankerengineer’s Expo app at
app.clanker.engineer; CLANKERNET’s Expo app atapp.clanker.net(notfactory.clanker.net). - Landing pages standardised: clankerengineer drops
/mobileand/deployed-modeand is brandedclanker.engineer; CLANKERNET gets the same-shaped landing under theclanker.netbrand and owns deployed mode — the FACTORY’s BYOC tier, per D25: what is deployed into a customer’s cloud is a factory (host, containers, gateway) for the customer’s own repo, never the Clanker product. CLANKERNET production runs on ReplitSUPERSEDED 2026-09-16 (D22): CN production is trunk-deployed by the factory itself — gateway on the factory host, Pages for site + app; no Replit.- clankerengineer will not be distributed to enterprise: deployed mode moves to
CLANKERNET and
deploy/ENTERPRISE.mdis deleted (T116), its load-bearing BYOC content re-homed in CN docs. “Moves to” means the TIER moves (D25) — clankerengineer itself is never deployed to a customer; CN’s tier deploys the factory.
Repos on this machine: /home/developer/clanker (clankerengineer, develop),
/home/developer/clanker-gitfwd (worktree of feat/gitfwd-coordination),
/home/developer/clankernet (CLANKERNET, develop), /home/developer/gitfwd (GitFWD, main —
a PRIVATE repo: unauthenticated API → 404).
Evidence: a 6-reader / 3-design / 2-judge workflow (wf_e801cb44-9ca; maps + designs at
/tmp/claude-1001/.../tasks/{map1..6,design1..3}.md), a 4-lens adversarial critique of the
first draft (wf_549e4163-1c6; tasks/critique.txt), and hand reads. Every file:line
below was cited by a reader/critic or verified by hand.
What the product IS
A product factory (D25, USER 2026-09-17): from a factory.yml it stands up
engineering environments — a host, Traefik-fronted developer containers, the GitFWD
coordination gateway — on the adopter’s OWN infrastructure, for whatever repository the
adopter is building. It is NOT “Clanker, the product, hosted in your cloud”:
clankerengineer is a separate, hosted sister product at clanker.engineer and is never
deployed to a customer. Anything in this tree that says or implies otherwise is wrong.
Three faces, one repo:
- The ENGINE —
packages/engine: afactoryCLI (TypeScript, Pulumi Automation API with the provider SDKs as its own dependencies) that turnsfactory.yml(WHAT to run — owned by the adopting repo) plus a host spec (WHERE — accounts, tokens, state; owned by the host owner) into a Traefik-fronted Docker host and per-developer containers. Provider seams with stated v1 implementations: Compute (hetzner,existing-host), Ingress (cloudflare-tunnel), DNS (cloudflare, comment-tagged ownership), Access (tailscale,ssh-direct), Secrets (pulumi-esc,pulumi-config,factory-secrets), State (pulumi-cloud,s3incl. R2 via?endpoint=). - The CONTROL PLANE — a reusable GitHub Actions workflow
(
clankerlabs/CLANKERNET/.github/workflows/factory-deploy.yml@<sha|v1>) executing in the CUSTOMER’s repo with the customer’s credentials. Files are reached through a composite action (${{ github.action_path }}/../../..), the only mechanism that works for a private CLANKERNET without a cross-repo token. No hosted service holds customer cloud credentials. v1 (T001–T082) is INTRA-ORG ONLY: a private repo’s reusable workflow, composite action and GHCR packages are reachable only from clankerlabs repos; the customer-facing product starts at T103 (go public). - The CLIENT — the Expo Router app at
app/, identitynet.clanker, served onapp.clanker.netby the GFP/1 gateway from a Replit deployment (D17). v1 = the coordination UI + “Login with GitHub”; Machines/dispatch is v1.1.
BYOC, mechanically:
- Pulumi STATE lives in a backend the host owner chooses (
pulumi-cloudunder THEIR org, ors3/R2 + passphrase/KMS). State necessarily holds host secrets (SSH key, tunnel token) — the control is WHO holds the backend and the decryption key, not “no secrets in state”. Withpulumi-cloudthe owner accepts Pulumi Inc. as a key holder; documented. - Provider tokens and host secrets live in the host’s Secrets provider, never as stack
OUTPUTS (
no-secret-outputstest) and never as committedsecure:blobs — that is the exact patterndeploy/ENTERPRISE.md:315-316lists as an open violation forPulumi.staging.yaml, and CLANKERNET must not inherit it (D3). - DNS: only records in the owner’s zone(s) carrying
comment: managed-by=factory/<host>are ever touched; per-CONTAINER records are idempotent API writes by the container engine (so N provisions never contend for one Pulumi stack); only HOST records are Pulumi resources; the reconciler’s DELETE/PUT path asserts the tag in code (unit-tested with an untagged fixture). Thefactory/appgateway record is{ protect: true }. - Authorisation to provision = the caller repo’s own
workflow_dispatchpermission, optionally tightened byaccess.github.orgoraccess.github.users. - Host-provisioning tokens never enter a container; what a container gets is exactly
the names in
env.secrets.
Why NOT the outputs-as-vault pattern (deploy/pulumi/index.ts:125-168,249-372): it
exists because Pulumi.dev-host.yaml is gitignored (.gitignore:83-88,
index.ts:159-162) so pulumi config returns {} in GHA — NOT because “the whitehatgg
org cannot decrypt config” (stale memory note; dev-host-container.yml:411-436 decrypts
staging config in GHA today). The fix is a secrets provider that CI can read with the one
token it has: Pulumi ESC (D3).
Migration strategy: ADOPT the live host’s state (USER DECIDED). The only thing that
can replace hcloud.Server("dev-host") is a userData or sshKeys diff
(deploy/pulumi/compute/dev-host.ts:251-261; both are ForceNew in the hcloud provider) —
cloud-init runs at first boot only, so the program declares ignoreChanges: ["userData"]
protect: trueon an adopted server, keeps the SshKey resource byte-identical, and later host changes go through the idempotent SSH converge. The legacy checkpoint is exported, URNs rewritten (clanker-infra→clankernet, stackdev-host→clankerlabs-hel1), imported into the new stack, gated onpulumi preview= 0 replace / 0 delete / 0 create. The box, tunnel, Traefik, R2 bucket, Upstash, everydev-*.clanker.netrecord and every running engineer container are untouched. The factory ADOPTS running containers by generating compose with the same project name (dev-{user},dev-host-container.yml:568) and volume keys (repo-data,postgres-data,tailscale-state,ssh-keys,docker-data;docker-compose.dev-env.yml:131-172), sodocker compose up -dreconciles rather than recreates. Legacy resource NAMES (clanker-dev-host, …) stay. Plan B, only if the gate shows a replace: a second host withdev2-{user}hostnames (isolation verified: legacy reconcilerdev-host-server.yml:669-716matchesstartsWith('dev-'); orphan sweeps match exact names) — documented, not built.
Decisions
| # | Decision | Resolution |
|---|---|---|
| D1 | CLANKERNET visibility (USER) | Private now. v1 is intra-org. Before any external adopter: T103 moves Clanker’s coordination lanes to a private clankerlabs/coordination, then CN + GHCR packages go public. T121’s tier page must say “operated by us (now) / self-serve (when public)”. |
| D2 | ~~Adopt the live host’s state (above). Legacy workflows FROZEN (T070) before adoption (T022); legacy stack pulumi stack rm --force after import (state only). | |
| D3 | SUPERSEDED 2026-09-16 (D24, USER): same account whitehatgg, project clanker-infra REUSED (one project, the development/staging/production family), secrets as pulumi config set --secret blobs committed in the stack file (the pulumi-config provider; ESC stays in the engine as a BYOC option only). Original: State: Pulumi Cloud, project clankernet, stack clankerlabs-hel1, in the existing whitehatgg account (USER, 2026-09-16: CE and CN are two projects in one account; the separate-org idea below is deferred, not required) clankerlabswhitehatgg with the org name in the host spec, never spelled in code). Secrets: Pulumi ESC environment clankernet/clankerlabs-hel1 referenced from a BLOB-FREE pulumi/Pulumi.clankerlabs-hel1.yaml (environment: [clankernet/clankerlabs-hel1] + non-secret keys only). CI reads it with PULUMI_ACCESS_TOKEN alone. Recorded as a deliberate departure from ENTERPRISE.md §8.1’s “no ESC” (that rule was about the Clanker PRODUCT’s customers; here the host owner IS the customer). BYOC alternatives: s3+passphrase (FACTORY_PASSPHRASE + AWS_* = 3 caller secrets) or the owner’s own Pulumi org + ESC. | |
| D4 | R2 bucket clanker-storage-dev-host | Comes with the state import. Adopted as { protect: true }; the empty-r2-before-delete Command (shared/storage.ts:20-37) is state-deleted and NOT ported into the default program — factory host down refuses while protected resources exist; factory resources destroy --host H --name N is the only path that empties+deletes. Name unchanged (workers/workspace/wrangler.toml:261 binds it remote = true). |
| D5 | Teardown semantics | teardown.preserveVolumes default TRUE + explicit purge. Switching a running container from the legacy compose to the generated one is a provision over kept volumes (same hostname). |
| D6 | Expo app identity (USER, CLOSED) | CLANKERNET = net.clanker (name CLANKERNET, slug clankernet, scheme + iOS bundle + Android package net.clanker, new icons; class-CG disclosure substance unchanged). The Clanker Engineer app is DEPLOYED under engineer.clanker, so the repo’s app.json:7,11 (net.clanker) is stale — T112 aligns it with the deployed identity; no rule-5 exception is involved. |
| D7 | feat/gitfwd-coordination | Extract into CN; close the branch. CE keeps factory.yml coordination: and a short CE-authored gitfwd/SKILL.md §0; the full skills come from the base image via symlink (T030). gitfwd-runner ships-not-started in v1. |
| D8 | gfp.js coupling | git subtree of GitFWD at vendor/gitfwd/ + UPSTREAM_SHA + an OFFLINE drift test (subtree squash-commit trailers; GitFWD is private so a network fetch would always skip in CI). ONE pin serves app, gateway, CLI, MCP, hooks, e2e, skills. |
| D9 | Nothing else is called factory.yml | Traefik route file → host/traefik.gateway.yml → on-host dynamic/gateway.yml. Workflows are factory-deploy.yml (CN reusable, CE caller). Root factory.yml is the manifest, only. |
| D10 | dev-host Pages rehearsal site | DELETE (T115): clanker-public-dev-host + dev-host.clanker.net + the dev-host static-site environment. deployment/SKILL.md:739-750 already calls it known-broken; keeping it makes T113’s zone assertion unsatisfiable and its workflow reads legacy stack outputs that disappear. |
| D11 | Git credential in containers | v1: PAT in the host’s secrets provider (secret:GIT_TOKEN). GitHub App token is v1.1. |
| D12 | Hostname template for clankerengineer | dev-{user}.{zone} / {port}-dev-{user}.{zone} (client/config/domain.ts; client/__tests__/lib/domain.test.ts:31-36). {user}-{repo}.{zone} is the multi-repo default; CN dogfood uses {user}-clankernet.clanker.net. |
| D13 | GFP/1 actor identity | v1: two actors per human (container-minted + OAuth-minted). The runner posts NOTHING to lanes (a lane has one writer; the runner holds no key) — onboarding goes to the job summary only. One-lane handoff is v1.1 (T101). |
| D14 | CN housekeeping | Delete refs/heads/gitfwd-root (owner confirms); commit .gitfwd/node.json; every workflow branches-ignore: ['gitfwd/**']; NO gitfwd-notify.yml in v1 (the gateway’s poll is the live path; upstream calls the hook “a quota bonfire”); never a ref/dir/workflow named gitfwd without a slash; refs/tags/archive/clankernet-pre-gitfwd-2026-01-08 retained. |
| D15 | Domain split (USER) | Clanker product → clanker.engineer (registered; resolves to a registrar forwarder 15.197.225.128/3.33.251.168, NOT on Cloudflare — zone + NS change is a manual registrar step). CLANKERNET owns clanker.net. Dev containers stay on dev-{user}.clanker.net (USER) — they are factory-provided hosts on CLANKERNET’s zone; no per-user move, no hostname/DNS/env change. The zone move is product-only: staging (T111) and production (T112), independent of the factory tail; CN takes the clanker.net apex (T113) after T112. |
| D16 | Final hostnames (USER) | Clanker product: clanker.engineer, app.clanker.engineer, api.clanker.engineer, staging next./app-next./api-next.clanker.engineer. CLANKERNET: clanker.net (Pages landing, CN workflow), app.clanker.net (Pages: app/dist + a _worker.js proxying /gf/v1/* and /auth/* to the API host, same-origin like CE’s app site — CN workflow), api.clanker.net (Replit: gateway + auth-bridge behind front.js), CE dev containers dev-{user}.clanker.net (unchanged), CN dogfood {user}-clankernet.clanker.net. Rule (USER): Replit owns ONLY api.clanker.engineer and api.clanker.net; every other hostname is a Pages deployment published by its repo’s GitHub workflow. Both app.clanker.net and api.clanker.net are LIVE today as the product’s (HTTP 200 / Replit IP), so there is NO interim hostname: CN’s Replit deployment is verified on its *.replit.app URL and the dogfood gateway gw-hel1.clanker.net (T041) covers login until T113 attaches the real names. |
| D17 | CN production runtime (USER) | Replit, restored from archive/clankernet-pre-gitfwd-2026-01-08:.replit and adapted: deploymentTarget = "vm" (NOT autoscale — SSE streams, git poll loop, token file, identity map; autoscale is ephemeral + multi-instance). Losing the disk on redeploy costs the token file + identity map (re-login), nothing from the log. Replit serves api.clanker.net only; landing and app are Pages (D16). |
| D18 | GitHub OAuth app for the gateway | A DEDICATED CLANKERNET OAuth app owned by clankerlabs (T042). A GitHub OAuth app has ONE callback URL, so the branch’s borrowing of the staging/product app (clanker-gitfwd/.github/workflows/dev-host-server.yml:427-434) would have broken product login; auth-bridge.js:39,185,221 computes redirect_uri from PUBLIC_ORIGIN. Callback https://app.clanker.net/auth/github/callback (the bridge is reached through the app origin’s proxy); until T113 the production app is exercised only through the dogfood app on gw-hel1.clanker.net. Every BYOC customer registers their own. |
| D19 | SSH key custody | No secret outputs, so the host root key lives in the secrets provider: for an adopted host, copied ONCE from the legacy state (sshPrivateKeyBase64 --show-secrets) into ESC as hostSshPrivateKey/hostSshPublicKey, the tls.PrivateKey resource state-deleted, and hcloud.SshKey fed the SAME public key under the SAME logical name (no diff — sshKeys on the server is ForceNew). New hosts: keypair generated on the runner before the first up. |
| D20 | SSH authorized keys | Default access.ssh.keySource: github-keys (https://github.com/<login>.keys — the identity that authorised the provision); sshid (today’s AuthorizedKeysCommand, Dockerfile.dev:55-59, a third-party authorisation authority) is opt-in with a per-user access.ssh.users.<login>.sshid override; list for static keys. CE keeps sshid to preserve engineers’ passkeys. |
| D21 | deploy/ENTERPRISE.md (USER) | DELETED from clankerengineer (T116) — the Clanker product is not distributed to enterprise. What survives moves to CN: the BYOC trust model (§3.2/§5.1b → docs/byoc.md), the shared-responsibility matrix (§7.2), artifact provenance/SLSA/SBOM (§1.2b Tier 0 → docs/releasing.md, already in T051), and the “managed dedicated, operated by us, first” recommendation (§1.1 → the /deployed tier page). The compliance program (SOC 2/ISO/GDPR sections) is not carried — it belongs to whichever product sells it, and that is now CN’s decision to make later. The CE byoc DeploymentEnv (server/infra/profile.ts:277, deploy/pulumi/index.ts:625,644, byoc-profile.test.ts) is dead with it — removal is part of T116, flagged because it is runtime code. |
| D22 | CN deploy model (USER, 2026-09-16, SUPERSEDES D16’s Replit rule for CN and ALL of D17) | Trunk-based, factory-hosted, no Replit. CN’s production is deployed BY the factory from CN’s own factory.yml — the reference BYOC deployment. (1) api.clanker.net = the gateway as a host-stack SERVICE on clankerlabs-hel1 (T041 promoted from optional-dogfood to THE production runtime), image built with provenance (T051) and rolled out by CN’s own factory-deploy.yml (T050); rollback = redeploy the previous image tag. T114 is DELETED; nothing of CN runs on Replit (CE’s api.clanker.engineer on Replit is untouched). The T040 replit gateway mode + front.js single-port multiplexing lose their reason to exist — renamed to a generic single-port mode or removed, never left as dead Replit code. (2) One trunk, develop (already the default; every workflow filters on it). Push to develop (branches-ignore: gitfwd/** + job-level if on refs/heads/develop — see T122 erratum) → .github/workflows/deploy.yml: build + test → publish Pages production clankernet-public → clanker.net, clankernet-app → app.clanker.net (GATEWAY_ORIGIN=https://api.clanker.net) → smoke. The gateway rollout job joins the same workflow at T050 (needs factory-deploy.yml). No main, no next.clanker.net, no staging projects. (3) Staging = Cloudflare Pages PREVIEW deployments per PR (wrangler pages deploy --branch <head>, URL in the job summary) + a booted-container smoke of the gateway image in PR CI (T051). A real staging lane, if ever wanted, is a second hosts/*.yml — the BYOC multi-host feature (v1.1). (4) Customer-facing releases are TAGS (v1, v1.x, T051) cut from trunk — the promotion gate that BYOC users actually pin (setup-factory@v1, image digests). Named risk: production gateway and dev containers share one host in v1 (intra-org, D1); a dedicated clankerlabs-prod host later is one file. |
| D23 | Cutover = FULL TEARDOWN, no adoption, no carve-out (USER, 2026-09-16; SUPERSEDES D2, D4, D19’s adopted arm, and the ‘nobody moves’ half of D15) | CE’s dev-host infrastructure is destroyed by CE’s own teardown — server, firewall, keys, tunnel, DNS records, the Pages rehearsal site, and the R2 bucket clanker-storage-dev-host (emptied) and the Upstash clanker-redis — and the stack is pulumi stack rm’d. CN then builds clankerlabs-hel1 FRESH with factory host up (new keypair minted on the runner, new tunnel secret, new bucket + Redis created by the host program under the SAME names so workers/*/wrangler.toml bindings and CE’s factory.yml resources need no rename). Engineers push first, then re-provision through CN; hostnames dev-{user}.clanker.net are re-created (D15’s ‘stay on the zone’ half stands; its ‘no per-user change’ half does not — volumes, Tailscale devices, SSH host keys are new). The engine carries NO adoption machinery: adopted:, frozen legacy logical names, ignoreChanges on adopted inputs, rewrite-checkpoint.ts, the adopted golden and docs/operator/adoption-2026-09.md are DELETED (T024) — ‘bring your own Pulumi checkpoint’ is not a product feature; existing-host (bring your own VM) is and stays. T022/T062/T070/T071/T115 are replaced by ONE cutover task, T023, executed in a single announced window. Merge order: CN feat/factory-v2 → develop first (the two Pages projects must exist or deploy.yml fails by design), then CE feat/factory-adoption → develop after T080/T081 remove the dead code. |
| D24 | Naming standard: development / staging / production (USER, 2026-09-16) | ONE Pulumi account (whitehatgg), ONE project (clanker-infra), three stacks: development (CN’s factory host — replaces the legacy dev-host stack at T023), staging, production (CE). Secrets are pulumi config set --secret in the committed stack file, exactly as the other two stacks do — NO ESC for ours (the engine keeps pulumi-esc as a BYOC option). Resource names clanker-<thing>-<env>: clanker-storage-development, clanker-redis-development, server/firewall/key/tunnel prefix clanker-development; production stays unsuffixed (canonical names; a live bucket cannot be renamed). The legacy clanker-dev-host prefix is dropped — D23 creates everything fresh, nothing is preserved. Env identifier dev-host → development in CE runtime code (APP_ENV, server/infra/profile.ts, server/config/domain.ts, the three workers’ [env.dev-host] + bucket bindings, gen-worker-dev-vars.sh, tests) — executed inside T080/T081, since the deletion pass touches the same files. Directory standard: the mobile client is client/ in BOTH repos (CN’s app/ renamed). Revised same day (USER): the development rename is DEFERRED — host id, stack, prefix and bucket stay dev-host/clanker-dev-host/clanker-storage-dev-host for now (D23’s legacy-names clause stands); flipping to development later is one line in the host spec plus the CE rename in T081. Because the CN stack is therefore whitehatgg/clanker-infra/dev-host — the SAME name as the live legacy stack — the engine refuses to up/plan a stack holding resources it did not declare, and T023 step 3 (stack rm legacy) MUST precede step 4. The PULUMI_ACCESS_TOKEN is an ORG secret (clankerlabs → both repos), not per-repo. Supersedes D3 (org/ESC). |
| D25 | Positioning (USER, 2026-09-17) | clankerengineer is NEVER deployed into a customer’s cloud — no BYOC tier, no “need it in your own cloud” line, no deployed mode anywhere on its site. CLANKERNET is a DIFFERENT product: a product factory — it stands up engineering environments (hosts, developer containers, the coordination gateway) on the adopter’s own infrastructure from a factory.yml. What runs in a customer’s cloud is the FACTORY, never the Clanker product. CN’s /deployed page, docs/byoc.md prose and the landing must say exactly that; D21’s “deployed mode moves to CN” is reinterpreted as “the BYOC factory tier lives at CN”, not as Clanker-in-your-cloud. CE’s landing takes the operator-terminal STYLE the retired /mobile page had (T120 kept the old home.html look — wrong), same shell, no cross-sell framed as a deployment option; a sister-product footer link (“CLANKERNET — the factory”) is fine. |
| D26 | Configuration model: Dev Containers standard (USER, 2026-09-17) | Per repo: .devcontainer/devcontainer.json (the open spec — the repo stays openable in VS Code/Codespaces) with factory-only concerns under customizations.clankernet. Per host: factory.yml becomes the ORCHESTRATOR/fleet file (host spec + repos: [{url, ref, access}]) and factory sync converges the host from every repo’s devcontainer.json at its pinned ref. The image’s resolved-JSON contract is unchanged — the engine translates. Full plan: docs/plan/devcontainer-pivot-plan.md (T201–T210; T201–T204 before the cutover, T205–T207 after — there is no host to sync until T023). Supersedes the per-repo factory.yml manifest (T020/T060) after a one-release deprecation. |
Target CLANKERNET layout
CLANKERNET/ (develop; refs/heads/gitfwd/** never touched)
├── README.md product README; coordination-space section kept
├── factory.yml dogfood: CLANKERNET's own dev containers ({user}-clankernet.clanker.net)
├── .gitfwd/node.json
├── schema/factory.schema.json generated from packages/engine/src/schema
├── hosts/clankerlabs-hel1.yml host spec (NON-secret) — CN is this host's owner
├── pulumi/Pulumi.yaml project `clankernet` (no program here — the engine is the program)
├── pulumi/Pulumi.clankerlabs-hel1.yaml blob-free: `environment:` (ESC) + non-secret keys
├── packages/engine/ `factory` CLI + Automation API program + providers + host/container engines + provider SDK deps
├── packages/gateway-service/ entrypoint.sh, front.js (one-port proxy), auth-bridge.js, docker-compose.yml
├── app/ the Expo app (own package.json + lockfile; never a root workspace)
├── vendor/gitfwd/ git subtree of whitehatgg/GitFWD @ UPSTREAM_SHA
├── images/base/Dockerfile ghcr.io/clankerlabs/factory-base (generic layers of Dockerfile.dev + postgres + gitfwd at /opt/factory/gitfwd)
├── images/entrypoint.sh GENERIC container entrypoint + factory-start/factory-sync/factory-auto-sync
├── host/ traefik.yml, docker-compose.traefik.yml, docker-compose.gateway.yml, traefik.gateway.yml
├── site/ clanker.net landing + deployed mode (T121)
├── .github/workflows/ factory-deploy.yml (workflow_call), ci.yml, release.yml, dogfood.yml — ALL branches-ignore gitfwd/**
├── .github/actions/setup-factory/ composite: node 22, pulumi 3.253.0 (the ONE pin), engine deps; FACTORY_ROOT via action_path
├── docs/ quickstart, factory-yml, hosts, byoc (BOM + v1 limits + shared responsibility), dns-ownership, upgrading-gitfwd, releasing, site-standard, operator/
└── tests/ workflow-hardening, tool-pinning (scans actions/**), no-secret-outputs, no-committed-blobs, vendor-drift, vendor-shape, no-gitfwd-collision, sha-pinned-uses
factory.yml keys (full CE example + minimal Node example written in T060/T020):
version, repo{name,defaultBranch,path,user}, host{ref}, image{base,ref,pull.tokenFrom}, access{github{org?,users?}, ssh{keySource,users?}, tailscale{hostname}}, hostnames{zone?,default,port}, ports[], services{postgres,procfile,primary,tmuxSession}, resources{r2[],upstash[]}, env{file,static,templated,secrets[],computed[],git}, hooks{postClone,provision,sync}, agents{claude.mcp,plugins,codex}, features{innerDocker,mosh}, coordination{repo,space,actorLabel,cloneDir}, sync{branch,autoSyncSeconds,restart}, teardown{preserveVolumes}.
host.ref forms: ./hosts/<name>.yml (caller repo), repo:<owner>/<repo>@<ref>:hosts/<name>.yml
(host-owner repo; optional HOSTS_REPO_TOKEN), or stack:<org>/clankernet/<name> (container verbs
read only non-secret outputs + the secrets provider the host spec names). Compose
environment: and Traefik routes are GENERATED from the manifest.
Tasks
Phases: P0 skeleton · P1 vendor + app · P2 engine (host) · P3 engine (container) · P4 gateway service · P5 control plane + release · P6 CE adoption · P7 in-place move · P8 delete + docs · P9 GitFWD cleanup · P10 v1.1 · P11 domain split · P12 sites. Tags: [CN]=CLANKERNET, [CE]=clankerengineer, [GF]=GitFWD, [OP]=operator/manual.
P0 — CLANKERNET product skeleton
T001: [CN] Product skeleton on develop, lanes untouched
- Blocked By: []
- Details:
- Create the tree above (empty packages with package.json/tsconfig,
docs/stubs,tests/), rootpackage.jsonwith workspaces forpackages/*ONLY (neverapp/). .github/workflows/ci.yml:on.push.branches-ignore: ['gitfwd/**'],on.pull_request.branches: [develop, main]; same filter on every later workflow.- Commit
.gitfwd/node.json(GFP/1 §22.3). Delete remote branchgitfwd-root(D14, after confirmation). Rewrite README: product first, coordination section verbatim below. - Tests copied + retargeted from CE:
workflow-hardening.test.ts(concurrency group +permissions: contents: readon every workflow, DNS gate);workflow-tool-pinning.test.ts— CN copy scans.github/workflows/*.ymlAND.github/actions/**/action.yml, vacuity floor>= 1(CE’s floor> 5and workflows-only scan,server/__tests__/workflow-tool-pinning.test.ts:30-47, would fail on the single composite pin). Excludevendor/**from every workflow-scanning test. tests/no-gitfwd-collision.test.ts: no file/dir namedgitfwdat root; no workflow triggers ongitfwd/**.- Files:
/home/developer/clankernet/**(new) - Acceptance:
npm testgreen;git for-each-ref refs/heads/gitfwd/unchanged before/after; the CI filter is asserted by the collision test.
- Create the tree above (empty packages with package.json/tsconfig,
T002: [CN] Pulumi project clankernet, ESC environment, blob-free stack file
- Blocked By: [T001, T003]
- Details:
pulumi/Pulumi.yaml(nameclankernet, runtime nodejs — the engine’s Automation API program usesworkDir: pulumi/). Provider SDKs (@pulumi/hcloud,@pulumi/cloudflare6.19.0 exact,@pulumi/tls,@pulumi/command,@pulumi/random,@upstash/pulumi) go inpackages/engine/package.json, pinned to the exact versions indeploy/pulumi/package-lock.json(itspackage.jsonpins only cloudflare). NO neon.- Create ESC environment
clankernet/clankerlabs-hel1;pulumi/Pulumi.clankerlabs-hel1.yaml=environment: [clankernet/clankerlabs-hel1]+ non-secret keys. Seed the ESC env ONCE from the legacy stack (D3, D19) —hetznerToken,cloudflareApiKey(outputs,index.ts:1118-1143),tailscaleOAuthClientId/Secret(outputs),hostSshPrivateKey/hostSshPublicKey(fromsshPrivateKeyBase64 --show-secrets+ derived pubkey),R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY(fromdevBackends --show-secrets --json),upstashApiKey/upstashEmail(fromwhitehatgg/clanker-infra/stagingconfig — dev-host uses STAGING’s Redis today,dev-host-container.yml:344-356). NOTcfTunnelSecret— it is a config input (compute/dev-host.ts:94-100), not an output; the adopted tunnel keeps its secret in state, and a NEW host generates one (random.RandomPassword). - Add
tests/no-committed-blobs.test.ts: nosecure:key anywhere underpulumi/orhosts/. - Files:
pulumi/Pulumi.yaml,pulumi/Pulumi.clankerlabs-hel1.yaml,packages/engine/package.json,docs/operator/seeding.md,tests/no-committed-blobs.test.ts - Acceptance: in a clean clone with only
PULUMI_ACCESS_TOKEN,pulumi config get hetznerToken --stack clankerlabs-hel1decrypts (via ESC); the blob test is green;git grep secure: -- pulumi hosts→ none.
T003: [OP+CN] Operator prerequisites
- Blocked By: [T001]
- Details:
- CN repo Settings → Actions → Access = “Accessible from repositories in the clankerlabs organization” (it is the CALLED repo’s setting); org policy allows
clankerlabs/CLANKERNET/*if actions are restricted. - CN repo secret
PULUMI_ACCESS_TOKEN(record: personal token vs a team token scoped to projectclankernet; D3 org decision). - Tailscale: reuse
tag:dev-envfor the adopted host and its containers (no ACL change); record inhosts/clankerlabs-hel1.yml. - Start the registrar NS change for
clanker.engineer(T110 needs it; propagation up to 48h). - Hetzner: note the project’s quota; no new server is needed under adoption.
- Files:
docs/operator/prerequisites.md,hosts/clankerlabs-hel1.yml(tag) - Acceptance: a throwaway workflow in CE can
uses: clankerlabs/CLANKERNET/.github/actions/setup-factory@<sha>;dogfood.ymlseesPULUMI_ACCESS_TOKEN;dig NS clanker.engineershows Cloudflare nameservers (may land later; T110 waits on it).
- CN repo Settings → Actions → Access = “Accessible from repositories in the clankerlabs organization” (it is the CALLED repo’s setting); org policy allows
T004: [CN] Repo standards parity with clankerengineer (tooling, CLAUDE.md, hygiene)
- Blocked By: [T001]
- Details (added 2026-09-16 after a CE↔CN standards comparison; USER: yes):
- ESLint (flat config, CE’s
eslint.config.jsminus Expo/i18n rules;app/keeps its own Expo config) + Prettier (CE’s settings), root scriptslint,lint:fix,format,check:format; one formatting-only commit. - Husky + lint-staged with CE’s three hooks (pre-commit lint-staged, commit-msg conventional commits, pre-push typecheck + changed-file lint + changed tests), installed via
prepare. Conventional commits ENFORCED here from day one (CE’s hook exists but is not installed in the dev checkout and its last 15 commits ignore it — do not copy that state). CLAUDE.md(CE’s shape, concise): layout, commands, the non-negotiables (D3 blob-free stacks, D9 naming, D14 gitfwd lanes, D22 deploy model, no secret outputs, hooks env), the guard tests and what each pins..claude/skills/seed:factory-engine,gateway-service,site-standard,deployment(D22 lane) — each pointing at the docs that hold the detail, not duplicating them..dockerignore(root build context — T030 erratum),.editorconfig,.nvmrc=22,check:lockfile.tests/repo-standards.test.tspins all of the above exist and that every workspace is covered by lint + typecheck.- Files:
eslint.config.js,.prettierrc,.husky/*,CLAUDE.md,.claude/skills/**,.dockerignore,.editorconfig,.nvmrc,tests/repo-standards.test.ts,package.jsonscripts - Acceptance:
npm run lint && npm run check:format && npm run typecheck && npm testgreen; a non-conventional commit message is refused by the hook;git diff --statof the format commit touches no logic.
- ESLint (flat config, CE’s
P1 — Vendor GitFWD, move the app
T010: [CN] Vendor GitFWD as a git subtree with one pin + OFFLINE drift test
- Blocked By: [T001]
- Details:
git subtree add --prefix vendor/gitfwd <GitFWD url> <sha> --squashwhere<sha>=clanker-gitfwd/.claude/skills/gitfwd/UPSTREAM_SHA(8df1ede…). Writevendor/gitfwd/UPSTREAM_SHA.tests/vendor-drift.test.ts(offline): (1) newestgit-subtree-splittrailer ingit log -- vendor/gitfwd==UPSTREAM_SHA; (2)git diff-tree --quiet <squash-commit>^{tree} HEAD:vendor/gitfwd(no hand edits); (3) sha256 ofvendor/gitfwd/clients/web/app/gfp.js+ the purity assertions fromgfp.test.js:1204-1220(no require/import/export/Buffer/process/atob/btoa). Optional network re-check behindGITFWD_READ_TOKEN, never the only check.- EXPECTED until T091:
vendor/gitfwd/clients/expostill exists at this sha (GitFWD keeps it until T090) —app/metro.config.jsblockListexcludesvendor/**except theclients/web/appwatchFolder; CN workflow-scanning tests excludevendor/**. docs/upgrading-gitfwd.md:git subtree pull+ bumpUPSTREAM_SHAis the ONLY upgrade path.- Files:
vendor/gitfwd/**,vendor/gitfwd/UPSTREAM_SHA,tests/vendor-drift.test.ts,docs/upgrading-gitfwd.md - Acceptance: drift test green offline;
vendor/gitfwd/clients/web/gateway/selftest.shpasses (69 checks).
T011: [CN] Move clients/expo → app/ with history, rewire gfp.js, set identity once
- Blocked By: [T010]
- Details:
- History-preserving extraction (
git subtree split --prefix clients/expoin GF →git subtree add --prefix appin CN). Keeppackage.json+ lockfile intact (own package: expo-router; CE is React Navigation 7; both Expo SDK 57 — no version change). - Coupling rewires (exact):
app/src/gfp.js→module.exports = require('../../vendor/gitfwd/clients/web/app/gfp.js');app/metro.config.jswatchFolders: [path.resolve(__dirname, '../vendor/gitfwd/clients/web/app')];app/e2e/fixture.sh:17→ROOT=$(CDPATH= cd -- "$HERE/../../vendor/gitfwd" && pwd);app/e2e/run.js:31→const ROOT = path.resolve(APP, '..', 'vendor', 'gitfwd')(ROOT derives from APP=app/, not frome2e/). - Identity ONCE (D6, confirmed):
app/app.jsonnameCLANKERNET, slugclankernet, schemenet.clanker,ios.bundleIdentifier/android.packagenet.clanker; replaceapp/assets/*;src/disclosure.jsproduct name in prose only — HEADER/DETAIL/UNMET substance unchanged (spec §3.6 T-4 / §8.4 C0.10;e2e/run.jsimports HEADER). app/README.md: keep the class-CG declaration and single-source rule; replace “not vendored” with the subtree + drift-test contract.- CI job
appfrom GFci.yml:618-690:npm ci,node --test,EXPO_OFFLINE=1 EXPO_NO_TELEMETRY=1 CI=1 expo export --platform web, chromium fallback (ci.yml:676-684), e2e against the vendored gateway. - Files:
app/**,.github/workflows/ci.yml - Acceptance:
cd app && npm ci && npm test(59 cases) green;export:webproducesdist/index.html;npm run e2e31 assertions green againstvendor/gitfwd; every exported.htmlcarries the CLASS CG header.
- History-preserving extraction (
P2 — Engine: host
T020: [CN] Engine scaffold: factory.yml + host-spec schemas, host.ref resolution, JSON Schema, docs/byoc.md
- Blocked By: [T001]
- Details:
packages/engine: TypeScript; zodsrc/schema/factory.ts(manifest) +src/schema/host.ts(host spec:id, namePrefix, adopted?, compute{provider,…}, ingress{provider}, dns{zones[]}, access{tailscale{tag,oauthFrom}}, secrets{provider: pulumi-esc|pulumi-config|factory-secrets, …}, state{provider: pulumi-cloud{org}|s3{url,secretsProvider}}, image.pull.tokenFrom?, gateway?{host,coordinationRepo,org});src/cli.tsverbsvalidate | plan | host up|down|converge | container provision|sync|teardown|purge | resources destroy | secrets set | doctor. Required/optional lists grown fromdeploy/pulumi/shared/dev-host-preflight.ts(drop the-dev-hostbucket-suffix rule andassertDevHostAppSubdomain— Clanker-specific; the latter → CE test in T081).host.refforms (layout section) implemented;stack:form reads only non-secret outputs and the named secrets provider.- Schema refusals: ≥3-label hostnames without
advancedCertificate: true;ssh-directwithout explicitfirewall.ssh: true;ingress: none+public:ports → warning;services.postgres.versionmust be the base image’s shipped major (“image-provided”);access.githubneedsorgorusers;hostnames.zonemust be in the host’sdns.zones[]. schema/factory.schema.jsongenerated (zod-to-json-schema) with an up-to-date test.examples/factory.clankerengineer.yml,examples/factory.minimal-node.ymlvalidate.docs/byoc.md— the bill of materials and honest v1 limits (absorbing ENTERPRISE.md §3.2/§5.1b/§7.2, D21): Cloudflare account + zone (mandatory: only Cloudflare ingress/DNS providers), Hetzner token OR an Ubuntu 24.04 VM with root SSH, Tailscale tailnet + OAuth client (orssh-direct), Pulumi Cloud org + ESC OR s3 bucket + AWS keys + passphrase, a GitHub PAT (read:orgifaccess.github.org,repofor clone), their own GitHub OAuth app if they run a gateway, a GHCR pull token while packages are private; What we hold (nothing); What is in your state; the shared-responsibility matrix; v1 limitations each naming the task that lifts it (T102, T103).- Files:
packages/engine/src/{cli,schema/*,hostref}.ts,schema/factory.schema.json,examples/*.yml,docs/byoc.md,docs/hosts.md,packages/engine/test/schema.test.ts - Acceptance:
factory validate examples/*.ymlexits 0; each refusal has a failing fixture;npm run schema:checkgreen; a caller repo containing onlyfactory.yml+hosts/acme.ymlvalidates with no file from CN’shosts/.
T021: [CN] Host engine: Pulumi program + providers
- Blocked By: [T002, T020]
- Details:
src/program.ts(Automation API inline program,workDir: pulumi/) ported fromdeploy/pulumi/index.ts:69-377+compute/dev-host.ts+shared/{storage,cache,types}.ts. Cloud names{namePrefix}-…; for an adopted hostnamePrefix= the legacy names. Delete dead inputsletsEncryptEmail/githubPat(compute/dev-host.ts:13,21). Pintraefikandcloudflaredimage tags.- Providers:
compute/hetzner.ts(server + firewall SSH 22 + mosh UDP 60000-61000 + ICMP; SshKey public key from the secrets provider — D19;ignoreChanges: ["userData"]+protect: truewhenadopted),compute/existing-host.ts(customer VM + root key; same converge script),ingress/cloudflare-tunnel.ts(tunnel + catch-all → Traefik :80;factory validaterefuses ingress without TLS),dns/cloudflare.ts(HOST records only as Pulumi resources — the gateway record{ protect: true }; the container-record API reconciler with in-code tag assertion),access/tailscale.ts(OAuth → tagged preauth key minted ON THE RUNNER — never the OAuth client inuserDataascompute/dev-host.ts:158-159does today; host-device prune matcheshost.idEXACTLY +-Nsuffixes, never a prefix),access/ssh-direct.ts,state/{pulumi-cloud,s3}.ts,secrets/{pulumi-esc,pulumi-config,factory-secrets}.ts. - Tunnel token and Tailscale key delivered by the SSH converge, not cloud-init (metadata exposure). Cloud-init minimal (docker + tailscale + cloudflared); Traefik + sysbox via idempotent
host converge. ONE Traefik definition (host/traefik.yml, file provider). resources.r2[]/resources.upstash[]are host-stack resources with{ protect: true }; NO delete hook in the default program;factory resources destroyflips protect and runs the emptier (port ofempty-r2-bucket.js).assert-no-dns-deletionsported, run before every HOSTup/converge(container records are not Pulumi resources, so the gate never sees them).- Tests:
no-secret-outputs.test.ts(nopulumi.secret(in outputs;pulumi stack outputhas no secret values); golden rendered cloud-init fixture; adopted-host golden inputs (T022). - Files:
packages/engine/src/{program,host/*,providers/**}.ts,host/traefik.yml,host/docker-compose.traefik.yml,packages/engine/test/* - Acceptance:
factory plan host clankerlabs-hel1against the real account previews as in T022; for a FRESH host spec every hcloud/tunnel/upstash step’sinputs.namestarts with the spec’snamePrefix;no-secret-outputsgreen;pulumi stack outputshows onlyserverIp,tunnelId,tailscaleHostname.
T022: [CN+OP] Adopt the live host — DELETED (D23). Replaced by T023.
T023: [OP+CE+CN] Clean cutover: teardown → fresh host → re-provision → delete
- Blocked By: [T021, T024, T031, T050, T051, T061, T003]
- Details (one announced window; every step is a command in
docs/operator/cutover-2026-09.md, written BEFORE the window and rehearsed on paper):- Announce + freeze. Engineers push every branch;
gitfwdlanes are on GitHub, nothing to save. Snapshot for the record only:hcloud server list,pulumi stack exportofwhitehatgg/clanker-infra/dev-hostto the operator’s machine,wrangler r2 object list clanker-storage-dev-host | wc -l(so ‘we knew what we deleted’ is on file). - Seed CN’s ESC first (T002 list, MINUS every SSH-key and tunnel-secret item — a fresh host mints its own; MINUS
upstashApiKey/upstashEmail— the Redis is EXISTING (resources.upstash[].existing, Wave 7 erratum), seeded asREDIS_URL/REDIS_TOKENfrom staging’sapp:redisUrl/app:redisTokeninstead; PLUScloudflareAccountId), and create the two Pages projects. Verifyfactory plan host clankerlabs-hel1renders against the real account with 0 errors while the legacy host still exists (name collisions onclanker-dev-host, the SshKey, the tunnel and the bucket are EXPECTED here and prove the plan reads the right account; they clear at step 4). - CE teardown (MUST precede step 4 — CN’s stack has the same name,
whitehatgg/clanker-infra/dev-host, and the engine refuses a stack holding foreign resources):dev-host-server.yml action=teardownwith the DNS-deletion override (the gate must be overridden deliberately, with the reason string recorded), which empties + deletes the bucket, deletes Redis, tunnel, records, firewall, keys, server, and the rehearsal Pages site (D10 falls out for free). Do NOTpulumi stack rmit — CN reuses this exact stack name, and Pulumi Cloud keys the committedsecure:blobs to the stack; removing it drops the key. The CE teardown’spulumi destroyleaves it EMPTY, which the engine’s ownership guard accepts. Tailscale:tailnet-prune.shforclanker-dev-hostand everydev-*device. - CN
factory host up clankerlabs-hel1(withgateway.hostset to a TEMPORARY name underclanker.net— the product’sapi.clanker.netis still live; see Wave 3 errata) →host converge. Gate:pulumi stack outputshows exactly the four outputs;https://api.clanker.net-to-be tunnel hostname answers Traefik 404; bucket + Redis exist under the old names;assert-no-dns-deletionsran. - Re-provision every engineer via
factory-deploy.yml action=provision(CE caller, T061 image) — fresh volumes,db:migrate+builtins:pushrun by the hooks; eachhttps://dev-<user>.clanker.net/api/health/ready200; Tailscale ssh works;wrangler dev --env dev-host --remotebinds the new bucket. - Delete in CE (T080/T081 executed inside the window, not later): both dev-host workflows,
deploy/pulumidev-host modules,dev-entrypoint.sh,Dockerfile.devlegacy path, the two workflow tests,deployment/SKILL.mddev-host sections, rule 11 → ‘dev containers come from CLANKERNET’. Merge CEfeat/factory-adoption → develop.
- Files:
docs/operator/cutover-2026-09.md(CN), CE deletions per T080/T081 - Acceptance:
whitehatgg/clanker-infra/dev-hostholds only CN’s resources (pulumi stack export | jq '.deployment.resources|length');hcloud server listshows ONE server, created in the window; every engineer’s container answers on its old hostname;grep -rn "dev-host" .github/workflows deploy/pulumiin CE → none; CNnpm test+ CEnpx jestgreen; the operator doc’s every command was the one actually run (edited in place during the window).
- Announce + freeze. Engineers push every branch;
T024: [CN] Delete the adoption machinery from the engine (D23)
- Blocked By: [T021]
- Details: remove
adoptedfromschema/host.tsand every branch on it (ignoreChanges: ["userData"|"tunnelSecret"],protectflips tied to adoption, frozen legacy logical names — names become{namePrefix}-…unconditionally, withnamePrefix: clanker-dev-hostkept inhosts/clankerlabs-hel1.ymlso the fresh resources carry the legacy NAMES without legacy STATE); deletepackages/engine/scripts/rewrite-checkpoint.ts,test/adopted-host.golden.test.ts+ fixture, anyadoptionprose indocs/hosts.md/docs/byoc.md/seeding.md(the seed list loseshostSshPrivateKey/hostSshPublicKey/cfTunnelSecret; a new keypair is generated on the runner before the firstupand stored in the secrets provider byhost upitself).existing-hoststays. Bucket + Redis are ordinary host-stack resources,{ protect: true }, created by the program. - Acceptance:
git grep -n adopt packages/ docs/ hosts/ examples/→ none outside the plan; engine tests green;no-secret-outputsgreen;factory plan host clankerlabs-hel1(mocked) creates the server, key, firewall, tunnel, bucket, Redis withclanker-dev-host/clanker-storage-dev-host/clanker-redisnames.
P3 — Engine: containers
T030: [CN] factory-base image + generic entrypoint
- Blocked By: [T010, T020]
- Details:
images/base/Dockerfilefrom the generic layers ofdeploy/Dockerfile.dev:19-111PLUSpostgresql-15 postgresql-client-15(Dockerfile.dev installs postgres at :124 in the “repo layer” — it is generic) andrun-local-ci(inert without sysbox): node:22-slim, sshd with anAuthorizedKeysCommandscript that reads its SOURCE from env (github-keysdefault,sshidopt-in — D20), tailscale, docker-ce client,mosh(NEW — today only the firewall opens its ports;features.moshgates it), tmux/overmind, claude-code/codex/tsx,developeruser;vendor/gitfwd/{bin,mcp,hooks,skills}→/opt/factory/gitfwd;ENTRYPOINT ["/opt/factory/entrypoint.sh"].images/entrypoint.sh= the GENERIC skeleton ofdeploy/scripts/dev-entrypoint.sh: tailscale join (pin--stateful-filtering=falseand--accept-dns=falsein a golden test), sshd, clonerepo.pathwithenv.git, local Postgres iffservices.postgres(FATAL on failure), writeenv.file, runhooks.postClonethenhooks.provision, GFP/1 setup ONLY whencoordination:is set (clone, mint ONCE — never re-init a clone withgitfwd.actor.id; install the reference-transaction hook; every failure a WARNING; symlink/opt/factory/gitfwd/skills/gitfwd{,-board,-review}into$REPO/.claude/skills/when absent; write.mcp.jsonfromcoordination.{space,cloneDir}when absent; exportGITFWD_HOME,GITFWD_REPO, PATH), tmux windows fromservices.procfile.- Installs
/usr/local/bin/factory-start(Procfile → tmux),factory-sync(ffsync.branch, runhooks.sync, restartsync.restartwindows),factory-auto-sync(loop onsync.autoSyncSeconds) — replacingdev-entrypoint.sh:595-846’sstart-clanker/clanker-sync/dev-auto-sync. NOTHING Clanker-specific. - Files:
images/base/Dockerfile,images/entrypoint.sh,images/bin/*,images/README.md,packages/engine/test/entrypoint.golden.test.ts - Acceptance: image builds;
examples/factory.minimal-node.ymlboots to tmux with Postgres running andenv | grep -c GITFWD== 0; the branch’sgitfwd-factory.test.ts“dev container bootstrap” cases re-targeted atimages/entrypoint.share green;docker inspectEntrypoint ==/opt/factory/entrypoint.sh.
T031: [CN] Container engine: generated compose + routes, verify probes, provision/sync/teardown/purge
- Blocked By: [T021, T030]
- Details:
- Port
dev-host-container.yml:164-1193intosrc/container/*: identity (actor;access.github.orgvia a PAT from the host secrets —GITHUB_TOKENcannot read org membership;userslist needs no PAT), env resolution (static ∪ templated ∪ secrets ∪ computed ∪ reserved), generateddocker-compose.<project>.yml(environment from the env list; projectdev-{user}whenhostnames.defaultisdev-{user}.{zone}, else{user}-{repo}; volume keys exactlyrepo-data,postgres-data,tailscale-state,ssh-keys,docker-data— the ADOPTION CONTRACT, test-pinned), Traefik file-provider routes fromports[]+hostnames(fromdeploy/bin/generate-port-config.sh), env file 0600 root on the host,docker compose up -d,SSHID_USERNAMEfromaccess.ssh.usersoverride (default = login). src/container/verify.ts(the checks that turn a silently-dead env into a failed run, fromdev-host-container.yml:642-649,780-870): Traefik Host-header probe on the host (404/000 after 60s = fail); Tailscale join + sshd probe viadocker exec tailscale status; public probehttps://{hostnames.default}{ports[default].health}through the tunnel;features.innerDocker→ probe host runtimes, setruntime: sysbox-runcor WARN (never fail).- DNS: idempotent tagged API writes (create/PUT/DELETE only records with
comment: managed-by=factory/{host}); UNTAGGED legacydev-{user}records are ADOPTED (tag added) on first provision, never duplicated. Tailscale device purge/remove match the EXACT rendered hostname (name.split('.')[0] === hostname || /^hostname-\d+$/) — today’s substring/prefix match (:494,:899,tailnet-prune.sh:85) would deletedev-alicewhen removingdev-al. sync: ffsync.branch,hooks.sync, restart windows, refresh sshid/github keys, restart Tailscale (:960-1062).teardownhonourspreserveVolumes;purgeremoves volumes; both remove tagged DNS + the exact Tailscale device. Onboarding card → job summary ONLY (D13).- Concurrency key
container:{host}:{user}:{repo}. Port CE’sdev-host-container-workflow.test.tsassertions as behavioural tests over generated output; behavioural test for the Tailscale matcher with devicesdev-al,dev-alice,alice-repoA,alice-repoB. - Files:
packages/engine/src/container/**,packages/engine/test/container/* - Acceptance:
factory container provision --host clankerlabs-hel1 --repo examples/factory.minimal-node.yml --user <you>→ reachablehttps://<you>-api.<zone>+ssh developer@<hostname>over Tailscale;teardownkeeps volumes,purgeremoves them; a manifest whose default port has no listener FAILS with a Traefik-404 error; on a throwawaydev-<tester>project created with the LEGACY compose,provisionreportsRecreating(notCreating) and the volumes’CreatedAtare unchanged.
- Port
P4 — Gateway service
T040: [CN] Extract deploy/gitfwd/* into packages/gateway-service
- Blocked By: [T010, T011]
- Details:
- From
/home/developer/clanker-gitfwd:deploy/gitfwd/{entrypoint.sh,auth-bridge.js,README.md},deploy/docker-compose.gitfwd.yml,.claude/skills/deployment-ops/references/factory-gitfwd.md(→docs/operator/gateway.md),server/__tests__/gitfwd-factory.test.ts(→packages/gateway-service/test, re-pathed).traefik-factory.yml→host/traefik.gateway.yml(D9). - Parameterise
auth-bridge.js: org,PUBLIC_ORIGIN, space, OAuth client from env; importKEYSfromapp/src/settings.js(module.exports = { load, save, KEYS, DEFAULTS }), pinned by one test. Scopesread:user read:orgnow. front.js: a dependency-free one-port proxy (path-split/auth/*→ bridge :8471, else → gateway :8470) because Replit exposes one port; the vendored gateway stays unmodified.- Gateway + gfp.js run IN PLACE from
vendor/gitfwd. Imageghcr.io/clankerlabs/factory-gatewaywithapp/distbaked at release — removes boot-timenpm ciand the GitFWD/CLANKERNET clone PAT from customer hosts (a GHCR pull token is still needed while packages are private — T051).--apponly whendist/index.htmlexists (a failed UI build is NOT fatal). - Files:
packages/gateway-service/**,host/docker-compose.gateway.yml,host/traefik.gateway.yml,docs/operator/gateway.md - Acceptance: re-pathed
gitfwd-factorytests green (refuses to start without OAuth creds; org gate; token never in a URL; constant-time state cookie; gateway as opaque binary); local run serves/gf/v1/nodeand the app throughfront.json one port.
- From
T041: [CN] Self-hosted gateway as an OPTIONAL host-stack service (BYOC customers)
- Blocked By: [T023, T040, T042]
- Details:
- Amended (D22): no longer optional for clankerlabs — this IS CN’s production runtime, hostname
api.clanker.net(Traefik route inhost/traefik.gateway.yml, DNS record{ protect: true }in T021’s host program).gw-hel1.clanker.netis dropped as a hostname; the same service answersapi.clanker.netafter T113 and the tunnel URL before. Rollout viafactory-deploy.yml(T050) from CN’s own rootfactory.yml; rollback = previous image tag. host convergedeploysdocker-compose.gateway.ymlonly when the host spec hasgateway:; secrets from the host’s secrets provider. clankerlabs PRODUCTION is this service (D22): thegateway:block onclankerlabs-hel1answersapi.clanker.net(tunnel hostname until T113), with a weeklydogfood.ymlsmoke (/gf/v1/node200 + login) so the customer path stays green.- Files:
packages/engine/src/host/gateway.ts,hosts/clankerlabs-hel1.yml,.github/workflows/dogfood.yml - Acceptance:
curl https://gw-hel1.clanker.net/gf/v1/node200 via the tunnel; login works with the dogfood OAuth app; weekly smoke green.
- Amended (D22): no longer optional for clankerlabs — this IS CN’s production runtime, hostname
T042: [OP+CN] Register the CLANKERNET GitHub OAuth app(s)
- Blocked By: [T040]
- Details:
- Amended (D22): the OAuth app’s callback stays
https://app.clanker.net/auth/github/callback(the Pages worker proxies/auth/*toapi.clanker.net). The Replit secret list is gone;GITHUB_CLIENT_ID/SECRET,GITFWD_BRIDGE_SECRET,GIT_TOKEN,COORDINATION_REPO_URL,PUBLIC_ORIGINare host secrets in ESC consumed by the gateway service’senv.secrets. - Org-owned OAuth app
CLANKERNET— callbackhttps://app.clanker.net/auth/github/callback(unusable until T113; that is expected), scopesread:user read:org. Store id/secret in ESC (host) — the gateway service reads them as host secrets (D22). NEVER the staging/product app (D18).docs/byoc.md: every customer registers their own. - Files:
docs/operator/oauth.md, ESC env,hosts/clankerlabs-hel1.yml - Acceptance:
pulumi config get githubClientId --stack clankerlabs-hel1!= the staging stack’s value; product login onapp-next…unaffected; the product’s OAuth app lists nofactory.*/app.clanker.netcallback.
- Amended (D22): the OAuth app’s callback stays
P5 — Control plane + release
T050: [CN] Reusable workflow factory-deploy.yml, composite action, dogfood
- Blocked By: [T031, T041, T051]
- Details:
.github/actions/setup-factory/action.yml: setup-node 22, Pulumi 3.253.0 (the ONE pin),npm ciinpackages/engine, exportsFACTORY_ROOT=${{ github.action_path }}/../../....github/workflows/factory-deploy.yml(workflow_call): inputsaction(host-up|host-down|host-converge|provision|sync|teardown|purge|resources-destroy),host,manifest(defaultfactory.yml),ref; secretsPULUMI_ACCESS_TOKEN(optional),FACTORY_PASSPHRASE(optional),AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY(optional),FACTORY_SECRETS(optional JSONname→value— thefactory-secretsprovider; aworkflow_callhas no open-ended secrets map, sogithub-environmentas a provider is dropped from v1),HOSTS_REPO_TOKEN(optional).permissions: contents: read, packages: read(noid-token: writeuntil OIDC lands in T102). Concurrencyhost:{host}/container:{host}:{actor}:{repo}. Steps: checkout caller atref→ setup-factory →factory validate→env.computed→ login backend → verb →no-secret-outputsgate → summary.dogfood.yml:workflow_dispatchcaller using./.github/workflows/factory-deploy.yml(local ref, so it tests the tree under change) with afactory_refinput for the composite; runs CN’s ownfactory.yml({user}-clankernet.clanker.net,image.baseonly,services.postgresoff,coordination:on); weekly gateway smoke (T041).tests/sha-pinned-uses.test.ts: everyuses:insidefactory-deploy.ymland the composite is SHA-pinned.docs/quickstart.mdrecommends@<40-char sha>+ Dependabotgithub-actionsfor adopters;@v1only for dogfood.- Files:
.github/actions/setup-factory/action.yml,.github/workflows/{factory-deploy,dogfood}.yml,factory.yml,docs/quickstart.md,tests/sha-pinned-uses.test.ts - Acceptance:
dogfood.yml action=provisionprovisions a CN container onclankerlabs-hel1withPULUMI_ACCESS_TOKENas the only secret; hardening + tool-pinning + sha-pinned tests green.
T051: [CN] release.yml: images with provenance, v1 tag discipline, GHCR access grants
- Blocked By: [T030, T040]
- Details:
- On tag
v*: build + pushghcr.io/clankerlabs/factory-base:<tag>,:1andfactory-gateway:<tag>withactions/attest-build-provenance+ SBOM (ENTERPRISE.md §1.2b Tier 0, re-homed per D21); publishschema/factory.schema.json; move thev1andv1.xtags to the release commit (release.yml is the ONLY mover; tag protection onv*); release notes carry the workflow/action SHA. First tagv1.0.0-rc.1(which also createsv1). - Packages PRIVATE until T103: grant repo
clankerengineerActions access and the service-account user (app:githubToken,staging-deploy.yml:190) read on both packages; every adopter host needsimage.pull.tokenFrom(GHCR read:packages) — recorded indocs/byoc.mdas a v1 limitation. - Files:
.github/workflows/release.yml,docs/releasing.md - Acceptance:
git ls-remote origin refs/tags/v1→ latest v1.x commit; a directgit push origin v1is refused; CEbuild-dev-imagepullsFROM factory-base:1with the existing PAT; attestations visible on both packages.
- On tag
P6 — clankerengineer adopts the factory (no deletions yet)
T060: [CE] Author factory.yml, the env-writer hook, and the keyset diff
- Blocked By: [T020]
- Details:
factory.ymlfromexamples/factory.clankerengineer.yml: ports fromcontainer.json(5000 server default + health/api/health/ready; 8081 expo path-routed/_expo|/assets|/hot|*.bundle|*.map; 3000 private; 8787/8790/8789 public),services.postgres(15,clanker_dev+clanker_test, exportsDATABASE_URL/DIRECT_URL/TEST_DATABASE_URL),procfile: Procfile.dev,resources.r2=clanker-storage-dev-host(adopted),resources.upstash= the adoptedclanker-redis,env.static(APP_ENV=dev-host, PORT, NODE_ENV, INNGEST_DEV, AI_ROUTER, NANGO_GITHUB_INTEGRATION_ID, MASTRA_TELEMETRY_DISABLED),env.templated(EXPO_PUBLIC_DOMAIN, STATIC_SITE_URL, REACT_NATIVE_PACKAGER_HOSTNAME,*_WORKER_URLfrom{port:name}, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AI_GATEWAY_URL),env.secrets(the ~17 names +KMS_MASTER_KEY—gen-worker-dev-vars.sh:73needs it),env.computed→scripts/factory/resolve-e2b-template.sh(E2B_TEMPLATE=clanker-sandbox-staging, deterministic per.github/actions/resolve-sandbox-digest/action.yml:105),hooks.provision= [scripts/factory/write-dev-env.shFIRST (carriesdev-entrypoint.sh:92-118and:347-357: derivedKMS_MASTER_KEY,EXECUTOR_AUTH_TOKENfallback, AI router preset), db:migrate fatal, gen-worker-dev-vars fatal, drizzle push to test db, builtins:push, check:prod-boot, playwright install,npm i -g @expo/ngrok],hooks.postCloneinstallsscripts/factory/bin/{restart-server,wait-for-server}on PATH,hooks.sync(db:migrate),access.ssh.keySource: sshid,agents.claude.mcpexpo,features.innerDocker/mosh,coordination(CLANKERNET, core,dev-{user},/home/developer/coordination),teardown.preserveVolumes: true, hostnamesdev-{user}.{zone}/{port}-dev-{user}.{zone}on the host’s default zoneclanker.net(nohostnames.zoneoverride — engineers stay put).check-ci: keep via.claude/commands/check-ci.mdonly; parameterisecheck-ci.sh:7/:104fromrepo.name.scripts/factory/env-keyset-diff.sh:devBackends(index.ts:249-372) ∪ composeenvironment:∪ heredoc, MINUS an explicitdropped:list with a reason per key (dead passthroughsdocker-compose.dev-env.yml:99-115:PULUMI_ACCESS_TOKEN,NEON_ORG_ID,E2B_ACCESS_TOKEN,INNGEST_*_STAGING,SECRETS_MASTER_KEY,SESSION_SECRET,DIRECT_URL…) and acontainer-derived:list (postgres URLs,SESSION_SECRET,EXPO_PACKAGER_PROXY_URL,EXPO_PUBLIC_APP_URL,EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY,CLANKER_SKIP_BOOT_MIGRATE), must equalstatic ∪ templated ∪ secrets ∪ computed ∪ reserved; ALSO everyVAR=assignment indev-entrypoint.shoutside the heredoc must be an env key or owned by a named hook script.- Files:
factory.yml,scripts/factory/{resolve-e2b-template.sh,write-dev-env.sh,env-keyset-diff.sh},scripts/factory/bin/* - Acceptance:
factory validate factory.ymlexits 0; keyset diff empty after the declared subtractions.
T061: [CE] Rebase the dev image on factory-base under a NEW tag; thin caller; .mcp.json; skills pointer
- Blocked By: [T051, T060]
- Details:
deploy/Dockerfile.factory(new, besideDockerfile.devwhich stays untouched until T080):FROM ghcr.io/clankerlabs/factory-base:1; ONLY the repo layer (server_dist, node_modules, playwright, python venv, Procfile.dev); NOCOPY dev-entrypoint.sh, NOCOPY container.json, NOENTRYPOINT(inherited), no duplicate postgres apt; keep.npmrcon everyCOPY … package-lock.json(npmrc-skips-onnxruntime-download.test.ts:48-63).staging-deploy.ymlbuild-dev-image(:137-251) gains a second target →ghcr.io/clankerlabs/clankerengineer:factory;factory.yml image.refpoints at:factory.:devkeeps serving any legacy path until T080..github/workflows/factory-deploy.yml(~30 lines):workflow_dispatch { action, host: clankerlabs-hel1 }→uses: clankerlabs/CLANKERNET/.github/workflows/factory-deploy.yml@<sha>(SHA, not@v1), explicitsecrets: { PULUMI_ACCESS_TOKEN },permissions: {contents: read, packages: read}, concurrency group in the caller too..mcp.json:${GITFWD_HOME:-/opt/factory/gitfwd}/mcp/gitfwd-mcp.js(absent outside a container = warning)..claude/skills/gitfwd/SKILL.md= a SHORT CE-authored §0 (separate clone atcloneDir, remoteorigin, binaries at/opt/factory/gitfwd/bin) linking to the symlinked full docs — NOT the branch’s copy (its §0 is already wrong about the remote and it re-creates an unpinned second copy).- Files:
deploy/Dockerfile.factory,.github/workflows/staging-deploy.yml,.github/workflows/factory-deploy.yml,.mcp.json,.claude/skills/gitfwd/SKILL.md - Acceptance:
docker inspect --format '{{.Config.Entrypoint}}' …:factory==/opt/factory/entrypoint.sh;workflow-hardening.test.tsgreen with old + new workflows; line-by-line audit: every line ofdev-entrypoint.shmaps toimages/entrypoint.sh, afactory-*bin, or a named hook (this audit is T080’s precondition, done here).
T062: Seed host secrets; re-provision one volunteer IN PLACE — DELETED (D23); seeding is T023 step 2, provisioning is T023 step 5.
P7 — In-place move Cutover (D23: see T023)
T070: Freeze the legacy workflows — DELETED (D23); the legacy workflows are deleted outright in T023 step 6 (T080).
T071: Lazily converge every container — DELETED (D23); every container is re-provisioned fresh in T023 step 5.
P8 — Delete and rewrite in clankerengineer
T080: [CE] Delete moved provisioning code
- Blocked By: [T071]
- Details:
- Delete:
.github/workflows/dev-host-server.yml,dev-host-container.yml;deploy/pulumi/compute/dev-host.ts;deploy/pulumi/index.tslines 69-377 (if (cloud === "dev-host")…} else {at :378), imports:10,:12(preflight) and:25(createDevHost), theenvironment === "dev-host"guard:385-387(moved to a test, T081) —createStorage/createCache(:5-6) STAY (main stacks use them);deploy/pulumi/shared/dev-host-preflight.ts;Pulumi.dev-host.example.yaml;deploy/scripts/tailnet-prune.sh;deploy/traefik.yml;deploy/docker-compose.traefik.yml;deploy/docker-compose.dev-env.yml;deploy/bin/generate-port-config.sh;container.json;deploy/scripts/dev-entrypoint.sh;deploy/Dockerfile.dev(renameDockerfile.factory→Dockerfile.dev, retire the:devtag instaging-deploy.yml);.dockerignore:104-120negations for the entrypoint/container.json;deploy/DEV_HOST_SANDBOX_TTFO_TESTING.md;deploy/scripts/ttfo-measure.mjs;e2e/E2B_SKILL_EXECUTION_E2E.md;.gitignore:83-88; the dev-host arms ofbootstrap-pulumi-stack.sh(:123,159,260-300,399) andsync-pulumi-config.sh(:70-80,114,129,226-254,278) → hard refusal pointing at CLANKERNET. - Tests: delete
dev-host-server-workflow.test.ts,dev-host-container-workflow.test.ts,deploy/scripts/__tests__/dev-host-preflight.test.ts; splitdev-host-parity.test.ts(app-contract half →factory.yml: ports, Procfile bind addresses, wranglerremote = true, INNGEST_DEV; provisioning half deleted); adjustworkflow-hardening.test.ts:175-179,static-site-hostnames.test.ts:69-91,production-stack-shape.test.ts:102. - Files: as listed
- Acceptance:
npm run check:types && npx jestgreen;grep -rn "dev-host-server\|dev-host-container\|dev-entrypoint\|generate-port-config\|container.json\|deploy-dev-host\|dev-env.yml" --include=* . | grep -v node_modules→ only docs rewritten in T081 and runtimeAPP_ENV=dev-hostbranches.
- Delete:
T081: [CE] Rewrite docs, skills, rule 11; relocate the app-subdomain guard
- Blocked By: [T080]
- Details:
- Added (D24): rename the env identifier
dev-host→developmentacross CE runtime code and config:DEPLOYMENT_ENVS/APP_ENV,server/infra/profile.ts,server/config/domain.ts,routes/auth.ts,worker-proxy.ts,better-auth.ts,workers/*/wrangler.toml[env.dev-host]→[env.development]with bucket bindingclanker-storage-development,gen-worker-dev-vars.sh,Pulumi.dev-host.example.yaml→ deleted with the stack,factory.ymlenv.static.APP_ENV=development, every test namingdev-host.grep -rn dev-host→ only history/CHANGELOG. CLAUDE.md:/deployblock, Deployment table (dev-host → “provisioned by CLANKERNET fromfactory.yml”; drop thefactory.clanker.netrow), Rule 11 → “Dev containers are provisioned by CLANKERNET’s reusable workflow fromfactory.yml— never from this repo”, coordination subsystem note (binaries at/opt/factory/gitfwd).replit.md,README.md,deploy/DEPLOYMENT.md:86-104,deploy/.env.example:210-221,deploy/README.md,.claude/replit-agent-context.md:168-172,docs/plans/DEV_HOST_PARITY_PLAN.md(superseded banner; cited byprovider-profile/SKILL.md:607),e2e/helpers/global-setup.ts:36comment.- Skills:
deployment-ops(SKILL +references/dev-host-containers.md→ pointer + Clanker-runtime sections;references/secrets-management.mdkey matrix;references/factory-gitfwd.md→ moved),deployment,clanker-dev-loop(+references/local-ci.md:13-27),database/SKILL.md:122-148,server/infra/migrator.ts:37,416comments,base-layer,feature-skill-execution,workflow-system,provider-profile,sandbox-execution,scaffold-deploy-ci,.claude/agents/deployer.md; memory notesdev-host-pulumi-ci.md/pulumi-org-decryption.mdcorrected (root cause = gitignored stack file) and the Tailscale/sysbox notes moved to CNdocs/operator/. - STAYS (document as staying):
APP_ENV=dev-hostruntime branches (server/config/domain.ts:35,233,server/infra/profile.ts:184-206,server/index.ts:92,routes/auth.ts:86-95,routes/worker-proxy.ts:157-199,auth/better-auth.ts:164-229,workers/*/wrangler.toml [env.dev-host]),Procfile.dev,deploy/Dockerfile.dev(rebased),gen-worker-dev-vars.sh(also.replit:268-291),prod-boot-check.sh,scripts/local-ci.sh,scripts/app-origin-proxy.mjs, playwright/e2e helpers,client/__tests__/lib/domain.test.ts, everything Replit. assertDevHostAppSubdomain→server/__tests__/dev-host-app-subdomain.test.ts.- Files: as listed
- Acceptance:
grep -rni "dev-host-server.yml\|dev-host-container.yml\|deploy/gitfwd\|factory.clanker.net" CLAUDE.md replit.md README.md deploy/ .claude/ .agents/ e2e/ docs/ scripts/→ none;npx jestgreen; Skills Reference groupings still true.
- Added (D24): rename the env identifier
T082: [CE] Close feat/gitfwd-coordination
- Blocked By: [T081]
- Details: Do NOT merge. PR “superseded by CLANKERNET” linking the CN commits per file; close; delete the branch after the owner nods;
git worktree remove /home/developer/clanker-gitfwd. - Acceptance: branch gone; all 36 files of the branch diff accounted for in CN (T040/T030/T061) or deliberately dropped (the
.claude/skills/gitfwd/binbinaries).
P9 — GitFWD cleanup
T090: [GF] Remove clients/expo, fix the guards and docs
- Blocked By: [T011]
- Details: separate PR after CN CI is green:
git rm -r clients/expo;ci.yml:641-652guard → “no package.json anywhere”; delete the expo job:618-690andclients/expoentries in:120-135; drop.gitignore:14-20; editREADME.md:162-166,clients/web/app/README.md:255-268,clients/web/gateway/README.md:125,WHITEPAPER.md:1282,docs/scope-decisions.md:57; CHANGELOG entry. - Acceptance: GF
ci.ymlgreen with zero package.json in the tree;release.ymlunaffected.
T091: [CN] Bump UPSTREAM_SHA past the removal
- Blocked By: [T090]
- Details:
git subtree pull --prefix vendor/gitfwd … <new sha> --squash; updateUPSTREAM_SHA;tests/vendor-shape.test.tsassertsvendor/gitfwd/clients/expois absent andvendor/holds no package.json/lockfile; re-run drift + app e2e. - Acceptance:
ls vendor/gitfwd/clients→tui web; drift green;cd app && npm run e2egreen; exactly one Expo app in CN.
P10 — v1.1 backlog
T100: [CN] Machines screen + dispatch, factory doctor version badge
- Blocked By: [T050, T071]
- Details: Machines tab lists the user’s containers per repo (GitHub API runs +
factory-state.jsonartifact) and dispatches provision/sync/teardown with the USER’s OAuth token (repo/workflowscopes added to the CN OAuth app);factory doctorreports caller-pin / host / base-image skew.
T101: [CN] One GFP/1 actor per human
- Blocked By: [T071]
- Details:
coordination.identity: gateway; identity map persisted intorefs/heads/gitfwd/meta. Optionalfactoryactor for runner-posted onboarding.
T102: [CN] Second-wave providers
- Blocked By: [T050]
- Details: OIDC for s3 backends (
aws-role-to-assume+id-token: write),aws-secrets-manager,caddy-directingress, GitHub App token forenv.git,existing-hostend-to-end test,gitfwd-runnerstart-up, archive-tag ssh2 gateway evaluation.
T103: [CN+CE] Split coordination lanes; go public
- Blocked By: [T071, T002]
- Details: new private
clankerlabs/coordination;coordination.repoin CE + CN dogfood + ReplitGIT_TOKENscope → the new repo; the existinge2e-verify-*lane in CLANKERNET is never deleted/force-pushed. Precondition already met by D3: nosecure:blobs ever committed. Then CN public + GHCR packages public; T121’s tier page flips to “self-serve”.
P11 — Domain split (D15/D16)
clanker.net is referenced in ~150 files of CE (33 skills, 24 tests, 12 site/src, 12
pulumi, 7 workflows, wrangler routes, eas.json, .replit, smoke scripts). Runtime is
env-driven (client/config/domain.ts:1, client/lib/query-client.ts:90,
server/config/domain.ts:107 fall back to clanker.net only when unset;
DEV_BASE_DOMAIN = dev.${APP_DOMAIN} :272).
T110: [CE+OP] clanker.engineer zone on Cloudflare (product zone only)
- Blocked By: [T003]
- Details:
- Cloudflare zone
clanker.engineeras a Pulumi resource in CE’sdeploy/pulumi(staging stack; the product’s zone). Wait for ACTIVE + Universal SSL after the NS change (T003). The factory host’s runner token stays scoped toclanker.netonly — dev containers do not use this zone. - Files:
deploy/pulumi/shared/dns.ts(+ zone),docs/operator/zones.md(CN, records thatclanker.engineeris the product’s, not the factory’s) - Acceptance:
curl https://probe.clanker.engineer(throwaway proxied record) returns a Cloudflare cert; zone id recorded inPulumi.staging.yaml.
- Cloudflare zone
T111: [CE] Staging → next.clanker.engineer family
- Blocked By: [T110]
- Details: Pulumi staging
app:domain/zone id (Pulumi.staging.yaml,sync-pulumi-config.sh,staging-deploy.yml);static-site.tscustom domains (next,app-next);assert-no-dns-deletionsexpectations; wrangler-nextroutes +zone_name(workers/*/wrangler.toml:76,:81,:197);ensure-workers.sh;e2e-staging.sh,smoke-static-site.mjs,smoke-dynamic-judgement.mjs; testsstatic-site-hostnames,worker-host-derivation,production-stack-shape;eas.jsonpreview;trustedOrigins; OAuth callbacks (GitHub app, Google, Apple services id) forapp-next.clanker.engineer; Stripe test webhook; Nango callback;.well-known/*,llms.txt,openapi.yaml; 52 doc/skill mentions ofnext.clanker.net. Keep*-next.clanker.netas 301s for one release. - Acceptance:
staging-deploy.ymlgreen end-to-end onnext.clanker.engineer; smoke scripts pass;grep -rn "next.clanker.net"→ only the 301 shim + CHANGELOG.
T112: [CE] Production → clanker.engineer + app.clanker.engineer; app re-identifies to engineer.clanker
- Blocked By: [T111]
- Details: Replit custom domain →
api.clanker.engineer; Pagesclanker-public→clanker.engineer; the Expo web app →app.clanker.engineer(executingHOSTNAME_SPLIT_PLAN.md’s marketing/app split on the new zone — theAPP_ROUTESregex coupling inscripts/build-static-site.tsis deleted, not ported); production Pulumi stack +production-apply-infrastructure.yml; wrangler production routes;eas.jsonproductionEXPO_PUBLIC_DOMAIN; Apple Sign-In services id + associated domains, Google, GitHub OAuth (product app only — no factory callback), Stripe live webhook (canonical/api/v1/billing/webhook; legacy aliases stay per CLAUDE.md), Nango prod;site/canonical URLs, sitemap, robots,_headers, SECURITY.md, README,PRODUCTION_RELEASE_RUNBOOK.md;smoke-production-runtime.mjs. App identity (D6): the app is DEPLOYED underengineer.clankerbutapp.json:7,11still saysnet.clanker— correct the repo to match the deployed identity (scheme,ios.bundleIdentifier,android.package=engineer.clanker;CFBundleDisplayNameunchanged); verify against the EAS project (f9fca744…) and store records before editing; deep-link scheme ripples intoclient/App.tsxlinking + maestro flows.clanker.net→ 301 shim in the existing Pages_worker.jsuntil T113; API clients getLocation+ a deprecation header for one release.- Sequence (2026-09-16, made concrete; USER: proceed). Production is RE-HOMED, never destroyed: Neon, R2
clanker-storage, Upstash, the Replit deployment, the three Workers, Pagesclanker-publicand the AI Gateway are hostname-agnostic and stay. Only NAMES move —app:domain, the 7DnsRecords, thePagesDomain, workerroutes, the Replit custom domain — and Pulumi moves them as REPLACEs (record name is ForceNew) that the DNS gate refuses untilALLOW_DNS_DELETION="<reason>"is passed deliberately. Order:- Zone ACTIVE + Universal SSL on
clanker.engineer(T110). Start the NS change today; up to 48h. - Register the new callbacks ADDITIVELY where the provider allows several: Google, Apple Sign-In return URLs (services id
engineer.clankeris already right —Pulumi.production.yamlapp:appleClientId), a SECOND Stripe live webhook endpoint athttps://api.clanker.engineer/api/v1/billing/webhook. GitHub’s OAuth app holds ONE callback → register a new product app forapp.clanker.engineer; its client id replacesapp:githubClientIdat step 4. - Ship a store/EAS build pointed at
api.clanker.engineerBEFORE the flip.EXPO_PUBLIC_DOMAIN/EXPO_PUBLIC_APP_URLare baked at build time; every installed app talks toapi.clanker.netand breaks the instant that name stops answering. This is the constraint the old brief missed. - Flip:
app:domain: clanker.engineer(+ zone id) inPulumi.production.yaml→Production — Apply Infrastructurewith the DNS override → workerroutes→*.clanker.engineerin all threewrangler.toml+ redeploy → Replit: linkapi.clanker.engineer, newapp:managedOriginVerifyTxt→ mergemainsoProduction — Releasepublishes the site → operator Replit redeploy. Downtime = DNS propagation. - Grace period on
clanker.net:api.clanker.netanswers a 308 (not 301 — a 301 turns every POST into a GET) toapi.clanker.engineer, apex +app.get a Cloudflare redirect rule toclanker.engineer, served from the old zone by Cloudflare (so it holds even if Replit cannot carry two custom domains — unverified). Ends when store adoption of the step-3 build is high enough. This grace period BOUNDS T113: CN cannot takeapi.clanker.netfor its gateway until it ends. - Remove the redirects; T113.
- Zone ACTIVE + Universal SSL on
HOSTNAME_SPLIT_PLAN.mddoes not exist in CE (T122 erratum) — the marketing/app split on the new zone needs its own short spec inside this task, not a reference.
- Sequence (2026-09-16, made concrete; USER: proceed). Production is RE-HOMED, never destroyed: Neon, R2
- Acceptance:
Production — Releasegreen onclanker.engineer; Replit redeploy serves on the new API host;curl -I https://clanker.net/→ 301; a fresh EAS preview build opensengineer.clanker://links.
T113: [CN] CLANKERNET takes clanker.net, app.clanker.net, api.clanker.net — Pages half DONE BY PIPELINE (2026-09-17)
- Blocked By: [T112 (+ the END of its step-5 grace period), T023, T041, T121, T122]
- Status (2026-09-17):
clanker.netandapp.clanker.netare attached AUTOMATICALLY bydeploy.yml’sattachstep on every production publish (no operator action, no Pulumi resource, no grace period — no production users exist, D25 context).api.clanker.netremains the host program’s gateway record (T023/T041) and still waits on T112’s grace period;hosts/dev-host.ymlkeeps its temporary name until then. Wave-7 erratum below. - Details: three hostnames flip in one window, after T112 has moved the product off them:
clanker.net→ Pagesclankernet-public(T121, CNdeploy.yml);app.clanker.net→ Pagesclankernet-app(T122);api.clanker.net→ the host tunnel record for the gateway service (T041, D22) —PUBLIC_ORIGIN=https://app.clanker.netin the gateway’s host secrets. DNS records as CN Pulumi resources withALLOW_DNS_DELETION="<reason>"as the deliberate override for the replace — this is the one step that can take a public site dark; run the front-door gate. Remove T112’s 301 shim.dev-*.clanker.netrecords STAY (factory containers, tagged);dev-host.clanker.netgone by T115.- Amended (D22): attaches
clanker.net→clankernet-public,app.clanker.net→clankernet-app(Pages custom domains) andapi.clanker.net→ the host tunnel (a host DNS record, T021/T041). Nonext./app-next.names, no Replit. Until then the*.pages.devURLs + the tunnel hostname.
- Amended (D22): attaches
- Acceptance:
https://clanker.netserves the CLANKERNET landing;https://app.clanker.netserves the app,GET https://app.clanker.net/gf/v1/node200 through the proxy, GitHub login completes on the app origin;curl https://api.clanker.net/gf/v1/node200 direct; every record in the zone is a CLANKERNET host or a factory-tagged container record — none is a Clanker-PRODUCT host.
T114: [CN] Restore .replit — CLANKERNET production on Replit
- Blocked By: [T040, T011, T042]
- Details:
- DELETED (D22, 2026-09-16): CN does not run on Replit. Work done before the decision (
.replit,replit.md,docs/operator/replit.md,replit.test.tsstatic pins, README prose) is reverted; the entrypoint’sreplitmode is renamedsingle-portor removed (T040 follow-up, same session)..replitmust NOT exist in CN. git show archive/clankernet-pre-gitfwd-2026-01-08:.replit > .replit, adapted (D17):modules = ["nodejs-22"],[deployment] deploymentTarget = "vm",build = ["sh","-c","cd app && npm ci --prefer-offline --no-audit --no-fund && EXPO_OFFLINE=1 EXPO_NO_TELEMETRY=1 CI=1 npx expo export --platform web && cd .. && rm -rf app/node_modules"],run = ["sh","-c","exec sh packages/gateway-service/entrypoint.sh"],[[ports]] localPort = 8470 externalPort = 80;hidden/[nix]trimmed (git, node); CE.replitconventions (comments per non-obvious line; no[agent]integrations; no phantom[workflows]).entrypoint.shReplit mode:STATE_DIR=${REPLIT_STATE_DIR:-/srv/gitfwd}; clone the coordination repo withGIT_TOKENat boot; gateway--listen 127.0.0.1:8470 --behind-tls-proxy+ bridge:8471behindfront.json the one exposed port.- Replit secrets:
GIT_TOKEN,GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET(the CN app, T042),GITFWD_BRIDGE_SECRET,GITFWD_ORG=clankerlabs,PUBLIC_ORIGIN=https://app.clanker.net(the bridge’sredirect_uriis on the APP origin, whose worker proxies/auth/*here).replit.mdrestored + rewritten. No custom domain until T113 — verify on the*.replit.appURL (login cannot complete there; that is expected —gw-hel1covers it). The gateway must accept the proxiedHost/X-Forwarded-*from the Pages worker (--behind-tls-proxy). docs/operator/replit.md: deploy = operator redeploy (site on Pages first, then runtime, same shape as CE); rollback = redeploy previous.- Files:
.replit,replit.md,packages/gateway-service/{entrypoint.sh,front.js},pulumi/DNS,docs/operator/replit.md - Acceptance: deployment green;
curl https://<deployment>.replit.app/gf/v1/node200; SSE stays open >5 min; a redeploy loses no message (gitfwd readcount before == after); login is verified end-to-end at T113.
- DELETED (D22, 2026-09-16): CN does not run on Replit. Work done before the decision (
T115: Delete the dev-host Pages rehearsal site — folded into T023 step 3 (the teardown destroys it); the CE code references (static-site-env.mjs, deploy-static-site.sh, check-static-site.mjs, smoke-static-site.mjs, smoke-dynamic-judgement, deployment/SKILL.md) are removed in T080/T081.
T116: [CE] Delete deploy/ENTERPRISE.md and the byoc deployment env (D21)
- Blocked By: [T020, T120]
- Details:
- Precondition: CN
docs/byoc.md(T020) anddocs/releasing.md(T051) carry what survives (§3.2/§5.1b trust model, §7.2 shared responsibility, §1.2b provenance); the/deployedtier page (T121) carries §1.1’s “operated by us first”. - Delete
deploy/ENTERPRISE.md. Rewrite every reference:SECURITY.md,SECURITY_AUDIT.md,.replit,.github/workflows/sandbox-build-image.yml,server/infra/profile.ts:122,252,server/config/domain.ts,server/services/push/types.ts,deploy/PRODUCTION_RELEASE_RUNBOOK.md,deploy/scripts/assert-state-clean.mjs:11,43(the §3.5 committed-blobs note becomes a self-contained comment — the script and itsno-secret-outputssibling stay, they gate staging/prod),docs/plans/DEV_HOST_PARITY_PLAN.md,docs/plans/workos-auth-provider.md,.claude/skills/{provider-profile,deployment-ops,deployment}/SKILL.md;server/__tests__/no-vendor-egress.test.ts:6,36,72,105,297keeps its property (no vendor call-home) with the doc pointer rewritten to CNdocs/byoc.md. - The
byocDeploymentEnv is dead code without a BYOC product: removeserver/infra/profile.ts:277(and thebyocmember ofDEPLOYMENT_ENVS), theenvironment !== "byoc"guardsdeploy/pulumi/index.ts:625,644(ENTERPRISE.md’s own note said to key on ownership, not name — with no byoc env the guard is simply gone),server/__tests__/byoc-profile.test.ts; grep"byoc"acrossserver/ shared/ workers/ client/ deploy/for stragglers. FLAGGED: runtime code — confirm before deleting. - Files: as listed
- Acceptance:
grep -rn "ENTERPRISE.md" --exclude-dir=node_modules .→ none;grep -rn '"byoc"' server shared workers deploy→ none;npx jest+check:typesgreen.
- Precondition: CN
P12 — Standardised landing pages (one shape, two brands)
Today site/src/html/home.html (“CLANKER.NET — The OS for your AI agents”, 1627 lines)
links /mobile (“clanker engineer — operator terminal”, 1211 lines) and four mode pages;
/deployed-mode (“Bring your own cloud … operated by us”, 799 lines) is the BYOC tier.
T120: [CE] site/: remove /mobile and /deployed-mode, rebrand to clanker.engineer, extract the shell
- Blocked By: [T121]
- Details:
- Delete
pages/mobile.astro,html/mobile.html,pages/deployed-mode.astro,html/deployed-mode.html(T121 copied the HTML first). home.html: fold the operator-terminal essentials (store CTA, “Download clanker.engineer” step:1147) into the landing; remove every/mobilelink (:1084,1112,1175,1457,1475), the Deployed pricing card:1437and footer link:1487;<title>“clanker.engineer — …”;og:*/twitter:*/keywords/canonical rebranded; 3-tier pricing (BYOK/PAYG/BASED) + a one-line “Need it in your own cloud? → clanker.net”.external-agents.htmlanddocs/concepts/workspaces.mdlose deployed-mode references.- Extract header/footer/nav/theme boilerplate into
site/src/lib/site-shell.ts(the “standard”; T121 consumes the same file). - Page lists:
scripts/build-static-site.ts:489,498,scripts/check-static-site.mjs:14,23, generated_worker.jsprefixes,site/README.md,deploy/STATIC_SITE.md; testslanding-install-snippet,base-layer-wire,no-client-llm-sdk,workflow-base-layer-resolvewhere they enumerate pages. - Files: as listed
- Acceptance: static build green;
check-static-site.mjspasses with the two pages absent;curl -I https://next.clanker.engineer/mobile→ 404 from the Pages worker (not the landing — the expo-asset fallthrough rule); nodeployed-modestring insite/.
- Delete
T121: [CN] site/: the clanker.net landing + deployed mode, same shell
- Blocked By: [T001]
- Details:
- Amended (D22): no
site-deploy.yml, no staging hostname. The site is published by.github/workflows/deploy.yml(T122) on push todevelop, with per-PR Pages previews. - Copy CE’s
site/scaffold (astro.config.mjs,_headers,remark-base-url.mjs,src/lib/render-html-page.ts) and — until T120 lands — the raw shell fromhome.html; copydeployed-mode.htmlNOW. Pages:/(“CLANKERNET — Deploy Your Own Clanking Machine”: what a factory is,factory.ymlin 20 lines, BYOC in one table, “Open the app” →app.clanker.net),/deployed(the moved deployed-mode page as the product tier: “operated by us (now) / self-serve via factory.yml (when public — T103)” per D1, absorbing ENTERPRISE.md §1.1’s managed-dedicated recommendation, linkingdocs/byoc.md),/docs/(from CNdocs/),/privacy,/terms. No/skills, no marketplace, no dollarinos. docs/site-standard.md: section order (hero → how it works → modes/pricing → CTA → footer), nav, theme script, meta/OG contract,static-route-manifest.jsonshape,check-static-sitepattern. CE’ssite/README.mdlinks it. A CN test diffs the shell against CE’ssite-shell.tsat a pinned CE sha (once T120 lands)..github/workflows/site-deploy.yml(CN): buildssite/and publishes Pages projectclankernet-public(wrangler pages deploy, Cloudflare token from ESC viaPULUMI_ACCESS_TOKEN), branch-filtered; custom domainclanker.netattached at T113 — until then the*.pages.devURL.- Files: CN
site/**,docs/site-standard.md, CNci.yml(build + check job) - Acceptance: CN site builds; copied
check-static-site.mjspasses; the tier page makes no claimdocs/byoc.mdcontradicts.
- Amended (D22): no
T122: [CN] app.clanker.net Pages project: app/dist + same-origin proxy worker + deploy workflow
- Blocked By: [T011, T040]
- Details:
- Amended (D22): replaces
app-deploy.ymlwith ONE.github/workflows/deploy.yml:on: push: branches: [develop]buildssite/+app/and publishesclankernet-public(→clanker.net) andclankernet-app(→app.clanker.net,GATEWAY_ORIGIN=https://api.clanker.net), smokes the Pages URLs, warns (never fails) while a custom domain is unattached;on: pull_requestpublishes both as PREVIEW deployments (--branch) and writes the URLs to the job summary. Noenvironment:gate (trunk is production; PR review is the gate). The gateway rollout job is appended at T050. app/pages/_worker.js(checked in, dependency-free): catch-all to the exportedindex.htmlfor app routes; proxies/gf/v1/*and/auth/*tohttps://api.clanker.net(configurable via a Pages env varGATEWAY_ORIGIN), streaming SSE unbuffered and forwardingAuthorization; the same shape as CE’s app-site worker inHOSTNAME_SPLIT_PLAN.md(“the app Pages project keeps a same-origin/api+/authproxy”). Expo asset misses → 404, never the shell (the CE fallthrough lesson).app/src/settings.jsdefault gateway stayswindow.location.originon web (already the behaviour when served by the gateway), so the app talks to its own origin and the worker does the hop..github/workflows/app-deploy.yml(CN):npm ciinapp/,expo export --platform web, copy_worker.js+_headersintodist/,wrangler pages deployto projectclankernet-app; branch-filtered,permissions: contents: read, concurrency; custom domain attached at T113.tests/app-worker.test.ts: the worker routes/gf/v1/spaces/core/eventsto the gateway origin with the SSE headers intact and answers 404 for a missing_expo/asset.- Files:
app/pages/_worker.js,app/pages/_headers,.github/workflows/app-deploy.yml,tests/app-worker.test.ts - Acceptance:
*.pages.devURL serves the app; withGATEWAY_ORIGINpointed at the*.replit.appdeployment,GET <pages>/gf/v1/node200; SSE frames arrive through the proxy.
- Amended (D22): replaces
Parallelism map
- Wave 1: T001 → {T003, T010, T020, T121} in parallel; T002 after T003.
- Wave 2: T011 (after T010) ∥ T021 (after T002+T020) ∥ T030 (after T010+T020) ∥ T060 (after T020) ∥ T120 (after T121).
- Wave 3: T040 (after T010+T011) ∥ T031 (after T021+T030) ∥ T042 (after T040) ∥ T051 (after T030+T040) ∥ T090 (after T011) → T091 ∥ T116 (after T020+T120) ∥ T122 (after T011+T040).
- Wave 4: T024 (after T021) ∥ T061 (after T051+T060) ∥ T110 (after T003; waits on the registrar). Then T023 (the cutover window — operator-gated; after T021+T024+T031+T050+T051+T061+T003), which executes T080/T081 inside it.
- Wave 5: T050 (after T031+T041+T051); T041 (after T023+T040+T042).
- Serial tail: T082 after T023 (T080/T081 run inside the window).
- Domain split: T111 → T112 after T110, independent of the factory tail; T113 after T112+T023+T041+T121+T122.
- v1.1: T100–T103 after T023.
Errata found during implementation (Wave 1, 2026-09-16)
- T010: the squash commit’s tree is the upstream tree at the ROOT, so
git log -- vendor/gitfwdnever lists it; the test finds it viagit log --grep=git-subtree-split.diff-treeagainstHEAD:vendor/gitfwddiffers by exactlyA UPSTREAM_SHA(the pin lives inside the prefix) — asserted as the ONLY difference. CI checkout needsfetch-depth: 0. - T011: the e2e has 30
check()sites, not 31 (upstream README shares the off-by-one). App CI lives in.github/workflows/ci-app.yml. Upstream’s “rest of repo is dependency-free” guard cannot be ported (CN has a root package.json by design) — the drift test carries that property forvendor/. - T001: CE’s real ts-jest is 29.4.12 (the
29.4.6string is a stale entry); jest kept at ~29.7.0..gitfwd/node.jsonwas generated by the vendored CLI (gitfwd init … --node-json), not hand-written. ci.yml installs Pulumi 3.253.0 in its test job so the tool-pinning floor is satisfiable at T001; T050 MUST replace it withuses: ./.github/actions/setup-factory(one pin site). - T020: zod 4 has built-in
z.toJSONSchema(nozod-to-json-schema). Thestack:host.ref form needs a non-secrethostSpecoutput — T021’s allow-list isserverIp, tunnelId, tailscaleHostname, hostSpec.container.jsonmarks 8081public: falsebut it is path-routed on the public host; schemapublic= “reachable through ingress” → true. The legacy entrypoint treats builtins:push / check:prod-boot / playwright / ngrok / drizzle-push as best-effort (dev-entrypoint.sh:468-545) — the example marks themfatal: false.STATIC_SITE_URL→https://{hostname}(the rehearsal site is deleted, D10).npx factoryon the public registry is an unrelated package — docs saynode packages/engine/bin/factory.js. - T040: two listeners cannot share 8470 —
front.jsowns${PORT:-8470}, gateway/bridge listen on 18470/18471 in Replit mode. The branch splicedGIT_TOKENinto the clone URL (persisted in.git/config) — replaced by a repo-local credential helper. The branch’s tokens-path fallback disagreed with the gateway’s default ($GIT_DIR/gitfwd/gateway-tokens).entrypoint.shneeds bash (re-execs itself undersh). The “dev container bootstrap” cases ofgitfwd-factory.test.tsbelong to T030, not T040.GATEWAY_MODE=replitis the explicit switch T114 sets. - T121: CE’s
site/has no package.json/lockfile (astro lives in CE’s root) — CNsite/is standalone (astro 5.18.2). Legal pages are NOT a text substitution (CE’s name Stripe/Apple/Dollarinos flows) — structure kept, content rewritten.render-html-page.tscannot useimport.meta.urlunder Astro (bundled into dist/).site/public/assets/site-chrome.js:3carries a wrong path comment copied byte-identical from CE — fix on the CE side in T120 (shared-by-copy rule).
Errata found during implementation (Wave 2, 2026-09-16)
- T004: CN
6ce18b0(tooling) +2b9efc4(format-only)..dockerignoreis an ALLOW-LIST:packages/gateway-service/Dockerfilealso builds from the repo root and COPYsapp/src/settings.js+app/dist, soapp/cannot be excluded wholesale;tests/repo-standardsevaluates it with moby’s matcher.deploy.ymlgates on amodeoutput computed inbuild(not a bare job-levelif) — CLAUDE.md states the real mechanism.typescript-eslint8.70 needs@eslint/json the eslint major (9.39.1).app/has no lint script → not in rootlint(pinned; flip when it gains one). Stale under D22/D23 and still to fix:docs/operator/seeding.mdlists the SSH-key/tunnel-secret seed items (T024 drops them);README.mdsays the gateway is served atfactory.clanker.net; the “Target CLANKERNET layout” still shows.replit/replit.md. Conventional commits are ENFORCED in CN; CE’s hook exists but is not installed in the dev checkout — align CE (install it) or the two repos disagree. - T021/T002:
@pulumi/pulumipinned 3.253.0 (CE lock has 3.223.0; the CLI pin wins); other SDKs match CE’s lock exactly. Theassert-no-dns-deletionsport drops theapp-site-dns/81058 pair (a Clanker Pages resource this program never declares).empty-r2-bucket.jshad a latent bug (XML keys not unescaped then re-escaped, so&-keys never deleted) — the port fixes it and THROWS on missing creds. Adopted tunnel:cfTunnelSecretis not seeded, so the program passes notunnelSecret+ignoreChanges: ["tunnelSecret"]; noRandomByteson adopted hosts (0-create holds).access/tailscale.tsis runner-only, not a Pulumi provider.compute.firewall.sshdoc is misleading: 22 is always open (converge runs over it);ssh: trueis only the ssh-direct acknowledgement. PulumisetMockshides resource options —test/helpers/pulumi-mock.tswraps the monitor to assertprotect/ignoreChanges.hosts/clankerlabs-hel1.yml(T003’s file) created here. Not done here: nopulumiCLI / token — ESC env + stack not created, nothing seeded, real-accountfactory plan hostNOT run (steps indocs/operator/seeding.md, which must also gaincloudflareAccountIdper the T122 erratum); the adopted golden carries a PLACEHOLDER public key and must be re-captured fromlegacy.json(UPDATE_GOLDEN=1) before the T022 gate.docs/hosts.mdcitesdns-ownership.md(T031, not yet written). - T060: CE
a6ef16a6b.check-ci.shlives atdeploy/bin/check-ci.sh(notscripts/), parameterised fromrepo.url(repo.namelacks the owner).container-derivedshrank to ONE key:EXPO_PACKAGER_PROXY_URL/EXPO_PUBLIC_APP_URLarehttps://{hostname}templates, postgres URLs areservices.postgres.exports,SESSION_SECRETandCLANKER_SKIP_BOOT_MIGRATEare DROPPED (no reader / always empty),DEV_BASE_DOMAINdropped too.EXECUTOR_AUTH_TOKENfallback is atdev-entrypoint.sh:307(inside the heredoc); compose dead passthroughs at:103-117. The/app/node_modulessymlink (dev-entrypoint.sh:379-383) was missing from the CN example — now a postClone hook; T061’sDockerfile.factorymust keep node_modules at/app. Engine gap (T030/T031): hooks runsudo -uwith only the env file, so RESERVEDGIT_TOKENis invisible togen-worker-dev-vars.sh:56(writes it as the marketplace worker’sGITHUB_TOKEN) — needshooks.env: [GIT_TOKEN]or the credential helper exposed to hooks.host.ref: repo:clankerlabs/CLANKERNET@v1:hosts/clankerlabs-hel1.ymlresolves nowhere untilhosts/exists at CN root (T021) andv1is tagged (T051). Adoptedclanker-owned databases go through the entrypoint’s ALTER/REASSIGN path (base image role isfactory).sync.autoSyncSeconds: 300vs legacy 30s — deliberate, flag if 30 wanted. - T122 (D22):
on: push: branches: [develop]is IMPOSSIBLE beside D14’sbranches-ignore: ['gitfwd/**'](GitHub forbids both on one event;no-gitfwd-collision.test.tsrefuses the allow-list form) —deploy.ymlusesbranches-ignore+ a job-levelif: github.ref == 'refs/heads/develop'.ci.yml/ci-app.ymlPR triggers narrowed to[develop](nomain).cloudflareAccountId(non-secret) must be in the ESC seed besidecloudflareApiKey(T002 list +docs/operator/seeding.md). Nothing creates the Pages projects — operator one-off:npx [email protected] pages project create clankernet-public --production-branch develop(andclankernet-app); the workflow asserts, never creates.GATEWAY_ORIGINdefault alreadyhttps://api.clanker.net— nothing to set./gf/v1/nodesmoke warning-only until T113.site/scripts/static-site-env.mjsstill acceptsSTATIC_SITE_ENV=staging(dead under D22).expo exportweb.output: staticemits per-route HTML;/assets/*is a fourth guarded miss shape. - T120: CE
241645cb7. The generated_worker.jsnever enumerated pages (“prefixes” had nothing to edit) — aRETIRED_PAGESlist now answers/mobile,/deployed-modewith a plain 404 (a 301 to/#get-started/clanker.net/deployedis an option after T113)./mobilehad consumers outsidesite/the plan missed:routes/activate.ts(app_download_url),routes/static.ts(sitemap),routes/agent-discovery.ts(llms.txt),e2e/screenshots/pages.spec.ts,replit.md, thefeature-authskill. Onlylanding-install-snippetenumerated pages; the other three named suites say “mobile” about the client.site-chrome.jssource isassets/(copied intosite/public/assetsat build) —site-standard.mdsays otherwise, fix on the CN side. CE has the FIRSTsite-shell.ts; CN’s copy + the pinned-sha diff test are still owed (T121 follow-up). Jest flag is--testPathPatterns(plural). Pre-existing:[...slug].astro:31hardcodes thehttps://clanker.netdocs canonical (T112). - T121 follow-up (CN
site-shell.ts+ parity test): CE241645cb7is the pin (tests/fixtures/site-shell.ce.sha; the file is unchanged at CE HEADa6ef16a6b). The parity slice is the RENDER half only — from the first export to the// Brand databanner — because CE’s header comment says “clanker.engineer(this repo)” and cannot be copied verbatim. CE’srenderFooterignoresNavLink.attrs, so the footer “The app” link lost itsdata-app-cta(the nav’s keeps it;check-static-sitereadsindex.html’s and passes) — fix on the CE side (renderExternal) and move the pin.NAV/FOOTERpresets carry the literalAPP_ORIGINtoken, sorender-html-page.tssubstitutes the shell BEFORE the site values (CE’s single loop happens to be ordered site-values-first, which works there only because its presets carry no token).docs/site-standard.mdclaimedscripts/check-static-site.mjsandsite-chrome.jslive undersite/on both sides — in CE both are ROOT (scripts/,assets/); fixed.STATIC_SITE_ENV=stagingdropped (production | preview, D22). The landing footer’s dead/docs/quickstartlink became/docs/“Overview”.tests/fixtures/is now excluded from eslint + prettier (byte-pinned golden, same rule aspackages/*/test/fixtures/).site/public/assets/site-chrome.jsre-synced byte-for-byte from CEassets/site-chrome.js(T120 fixed the:3path comment there). - T114 (deleted, D22): the T040
replitmode survives assingle-port— it is what the LOCALpackages/gateway-service/docker-compose.ymland the “Testing without Docker” recipe run (one port, no Traefik); productionhost/docker-compose.gateway.ymlrunsGATEWAY_MODE: dockerand neverfront.js. Mode selection is explicit (${GATEWAY_MODE:-docker}), state dir${GATEWAY_STATE_DIR:-/srv/gitfwd}. T041 gap:PUBLIC_ORIGIN(https://app.clanker.net) ≠gateway.host(api.clanker.net), so the engine must not derive one from the other — the host schema’sGatewayblock needs apublicOriginfield (or an ESC secret).COORDINATION_REPO_URLis a required gateway secret with no default. - T090: GF had TWO dependency guards, not one — the
gatewayjob’s step (ci.yml:596-605) was the only whole-tree guard once the expo job went, so it became the “no package.json anywhere” check (git ls-files '*package.json' '*package-lock.json'empty). Expo job actually spanned:611-692. Five more sites referenced the app (README.md:148-150,176-178,clients/web/app/README.md:5,clients/web/gateway/README.md:199,gitfwd-gateway.js:1667). Committed as GFe5d281conchore/expo-moved-to-clankernet(unpushed; T091 bumps the pin once it is on main). - T030: WIP
mf()usedjq -er, which exits 1 forfalseas well asnull, so every container with defaultfeatures:died underset -e— fixed. Multi-linerun: |hooks shifted intoname(one jq call for three fields) — fixed.PATH=cannot live in the env file (dotenv reads it literally) —gitfwdis on PATH via an image symlink;GITFWD_SPACEis exported too.agents.claude.mcpisSlug[]with no URL, so the entrypoint cannot register MCP servers — the legacyclaude mcp add expo …(dev-entrypoint.sh:480) goes to CE’s.mcp.json/provision hook (T061) or the schema grows{name,url}.dev-entrypoint.sh:92-118/:347-357are CE-specific (T060’swrite-dev-env.sh), not entrypoint material; the GFP/1 block lives in the branch worktree atdeploy/scripts/dev-entrypoint.sh:88-158+:485-497, not in develop’s file.tailnet-prune.shis host-side (T021/T031), nothing of it belongs in the image. Base image isnode:22-bookworm-slim(apt sources +postgresql-15are bookworm’s). Build context is the REPO ROOT (docker build -f images/base/Dockerfile .) — needs a root.dockerignore(T031). T031 contract: the entrypoint expectsFACTORY_MANIFEST(resolved JSON,actorLabel+tailscale.hostnamerendered; unrendered{…}is FATAL),FACTORY_ENV_FILE,FACTORY_USER(GITHUB_USERNAMEalias) — seeimages/README.md. Docker daemon unavailable on this box: image build/boot acceptance deferred.
Errata found during implementation (Wave 3, 2026-09-16)
-
T031:
hostDiris/opt/factoryfor every host (T024), so container files live at/opt/factory/containers/<project>/and routes at/opt/factory/dynamic/<project>.yml. RESERVED names reach compose through a 0600--env-file(${NAME:-}interpolation) so no credential is ever in the YAML;hooks.envis enforced engine-side (env -i+ only the allow-list indocker exec) and in the entrypoint (sudo --preserve-env=<names>);factory-syncruns hooks in a plain subshell.access.github.orgneeds host secretGITHUB_ORG_READ_TOKENfor provision AND teardown/purge. Provision does not purge the exact-name Tailscale device up front (D5); only the re-join fallback and teardown/purge do. Env-file values are single-quoted ('\''for a literal'). Apublic: falsedefault port → no DNS record, public probe skipped with a warning. Manifestresources.*.adoptedDELETED from the schema (strict object refuses it as unknown). Needs a real host:Recreatingover a legacy project with unchanged volumeCreatedAt; root-0600 bind mounts readable under sysbox-runc user-ns;sudo --preserve-envas root in the image; single-use authkey re-join; Upstash management API shape; Traefik 404-vs-forward. -
T024/T041:
gateway.host: api.clanker.netdeclares a CNAME while the PRODUCT’sapi.clanker.netis live in the same zone →host upcollides athost-gateway-dnsuntil T113. T023 step 4 must setgateway.hostto a temporary name underclanker.net(anddogfood.yml’shostdispatch input smokes the tunnel hostname) until T112’s grace period ends.plan hostmints the host keypair (the one side effect a plan has). Redis: specdatabaseName: clanker-dev-host(legacy Upstash name), manifest-facingname: clanker-redis. Commitfcf6902absorbed T024’s staged fixture deletions. -
T051: signed attestations are unavailable on private org repos (CE hit the same) — gated behind repo-public or
FACTORY_SIGNED_ATTESTATIONS=true; unsigned BuildKit provenance + SBOM referrers + syft asset always ship.github-actions[bot]cannot bypass a tag ruleset → the float move needs a GitHub App (releaseAppId/releaseAppPrivateKeyin ESC); operator creates App +v*ruleset TOGETHER. Trigger is release-shaped (v[0-9]+.[0-9]+.[0-9]+*), not barev*, because the App-token push of the floats would re-fire it.factory-gatewayalso gets:1/:1.x;docker-compose.gateway.ymldefaultsFACTORY_GATEWAY_TAG=1. Docker unavailable here: neither the ci smoke nor a release build has run. -
T121 follow-up: CE’s
renderFooterignoresNavLink.attrs(CN footer app link lostdata-app-cta) and CE’s single-pass substitution works only because its presets carry no token — fix both on the CE side, then move CN’s pin.site-standard.mdhad claimed CE keeps check/env/chrome undersite/; they are repo-root. -
T121/D25 (2026-09-17, USER): the tier page’s route is
/deployed-mode— its original clankerengineer name — NOT/deployed(what T121 shipped) and NOT/factory. Renamed together:site/src/pages/deployed-mode.astro,site/src/html/deployed-mode.html,site-shell.tsNAV/FOOTER data,prepare-static-site.mjsroutes,check-static-site.mjsrequired files,deploy.yml’s smoke path list (/deployed-mode/, trailing slash — Astro emits<route>/index.html),tests/{deploy-workflow,site-shell-parity}.test.ts,docs/site-standard.md, every in-site link. Copy reframed per D25 on the same day: the tier deploys the FACTORY (host, containers, gateway) for the adopter’s repo, never Clanker; the landing’s sister link reads “Building with Clanker? The hosted product is at clanker.engineer”. CE’s old ”→ clanker.net/deployed” pointer is deleted on the CE side (D25), nothing to keep in sync. -
T116: CE
a29338ed2+9547e0737. CE has NOno-secret-outputssibling (that is CN’s), andassert-state-clean.mjsis invoked by nothing. Pre-existing, out of scope:isRealDeployment("replit-production")isfalse, so Replit production trustsx-forwarded-host. -
T051:
release.yml+docs/releasing.md+scripts/release-tags.mjs(+tests/release-{workflow,tags}.test.ts, thegateway-imagejob inci.yml).actions/attest-build-provenancecannot run while CN is private: the attestation API is a paid feature on a private repo and CE’ssandbox-build-image.ymldeleted the step after “Feature not available for the clankerlabs organization” on every run — so the two signed steps are gated (verifyoutputattest: public repo, or repo variableFACTORY_SIGNED_ATTESTATIONS=true), BuildKit provenancemode=max+sbom: truereferrers and a syft SPDX SBOM (release asset) ship regardless, and the release notes say UNSIGNED until T103 or a plan change.github-actions[bot]cannot bypass a tag ruleset, so “release.yml is the ONLY mover” + “a directgit push origin v1is refused” needs a GitHub App as the bypass actor:releaseAppId/releaseAppPrivateKeyseeded in ESC (rule 9 holds — read withPULUMI_ACCESS_TOKEN), minted per run withactions/create-github-app-tokenscoped tocontents: write; until seeded the floats move withGITHUB_TOKENunder a::warning::, which works only while no ruleset exists (operator does ruleset + App together,docs/releasing.md§Operator). The trigger istags: ["v[0-9]+.[0-9]+.[0-9]+*"], not barev*: an App-token push STARTS workflows (GITHUB_TOKEN’s do not), so av*trigger would re-fire on the floats release.yml itself pushes and fail twice per release. A tags-only push trigger is exempt frombranches-ignoreinno-gitfwd-collision.test.ts(GitHub never fires it for a branch; adding the ignore besidetags:would make it fire on every branch push) — CLAUDE.md rule 3 reworded.factory-gatewaygets the:1/:1.xfloats too (the task named only:<tag>):host/docker-compose.gateway.ymldefaultsFACTORY_GATEWAY_TAGto1, so without them the default compose pulls nothing. The floats follow the HIGHEST release in their line by semver (pre-releases included — the plan’s “rc.1 creates v1”);v1.0.1behindv1.2.0movesv1.0only. The GHCR push credential is the ambientGITHUB_TOKEN(packages: writeon theimagesjob) — the package is created linked to the repo on first push; CE’s service-account PAT is a READ grant (docs/releasing.md§Operator), not a push credential.docs/byoc.mdalready carried theimage.pull.tokenFromlimitation line — unchanged. Not verified here: no Docker daemon on this box, so neither thegateway-imagesmoke nor a release build has executed; everyrun:block isbash -n-clean, the decide/notes/tags steps were dry-run in a temp repo. Gate results at hand-off: my scope green;packages/engine/test/cli.test.ts(4 container-verb cases), engine typecheck (test/container/helpers.tskeygen) and engine prettier are red from concurrent T031/T021 work in the same tree.
Errata found during implementation (Wave 5, 2026-09-16)
- T050:
factory-deploy.yml+.github/actions/setup-factory+ rootfactory.yml(+Procfile.dev) +dogfood.ymlcaller +docs/quickstart.md+tests/{sha-pinned-uses,factory-deploy-workflow}.test.ts+ enginetest/{dogfood-manifest,quickstart-doc,outputs-gate}.test.ts. A reusable workflow cannotuses:its own composite at its own SHA:uses:takes no expression, a./path resolves against the CALLER’s workspace, and thegithubcontext in a called workflow is the caller’s (workflow_shais the caller’s file; there is nojob_workflow_shain the context, only in the OIDC claim, which needs theid-token: writethis workflow deliberately does not hold). So the engine and composite come from a SECOND checkout —clankerlabs/CLANKERNETatfactory_refinto.factory/,uses: ./.factory/.github/actions/setup-factory— andfactory_refis a caller input that defaults to thev1float: an adopter pins it to the same 40-char SHA as theuses:line (docs/quickstart.md; Dependabot bumps only theuses:— the second pin is by hand, pinned byquickstart-doc.test.tsto match in the doc). While CN is private the engine checkout needs a token that reads CN —actions/checkoutwith the caller’sGITHUB_TOKENcannot clone a sibling private repo (the org “Actions access” setting coversuses:resolution only) — soHOSTS_REPO_TOKENdoubles as that credential (token: ${{ secrets.HOSTS_REPO_TOKEN || github.token }}); the dogfood caller is CN itself and needs nothing; lifted by T103. Concurrency is at the WORKFLOW level of the called workflow (GitHub’s documented place for it; a job-level group on the caller’suses:job misbehaves).workflow-hardening.test.ts’s “a composite may install pulumi” arm matched only./.github/actions/and now matches any local./…/.github/actions/path;deploy-workflow.test.ts’s “exactly one deploy workflow” list gainsfactory-deploy.yml(it matches/deploy/) and pins itworkflow_call-only. The composite’snpm ciisnpm ci --omit=dev --workspace packages/engine(271 packages, 3 s,factory --help+validate+planproven on a clean archive) —install: fullfor ci.yml/release.yml’sverify,install: nonewhere only the CLI is needed (deploy.ymlpublish, release.ymlrelease, which gained a checkout FIRST sogit cleancannot eat the downloaded artifacts); every previouscurl … get.pulumi.comis gone, the pin has ONE site.factory --helpused to exit 2 (the “no verb” arm); it is 0 now because the composite proves the load path with it.validate --host(and the workflow’shostinput) now accepts a host ID or a path like every other verb —isHostRefFormmoved tohostref.tsandcontainer/input.tsshares it;validate --jsongainedstack: {provider, name, backendUrl}(NON-secret, computed without the backend’senv(), which throws on a missing credential) so the workflow’spulumi login+ gate know the stack without a second parser;StateBackendgainedbackendUrl. The gate ispulumi stack output --json --stack … --cwd $FACTORY_ROOT/pulumi(the CLI wants a project) piped intopackages/engine/scripts/no-secret-outputs.ts, which sharessrc/outputs-gate.tswith the engine’s own post-upassertOutputKeys(one allow-list, two readers; it also refuses a[secret]value, which the engine’s check did not).resource(forresources-destroy --name) andallow_dns_deletion(the engine’s ONLY override, T113 will need it) are inputs the plan did not list. The dogfood manifest’s default port is the site’s astro dev server (4321, nohealth— nobody starts it on provision),Procfile.devis one window. Not done here: nothing has RUN on GitHub Actions — noPULUMI_ACCESS_TOKEN, no ESC environment, no host — sodogfood.yml action=provision(the T050 acceptance) is unexecuted; everyrun:block isbash -n-clean and every YAML parses. Unverified against a real runner:uses: ./.factory/.github/actions/…(a local action in a subdirectory of the workspace),actions/setup-node’s cache with the computed relative lockfile path,pulumi login/whoami --non-interactiveagainst an s3 backend, and whetherpulumi stack outputon a passphrase stack needsPULUMI_CONFIG_PASSPHRASE(it is passed anyway). The deploy.yml gateway rollout job is NOT appended (the marker stays):host-convergeon every trunk push has no host until T023.
Errata found during implementation (Wave 7 — checkout token, 2026-09-17)
- T050/T103:
HOSTS_REPO_TOKENdeleted 2026-09-17 — the checkout token is Pulumi config on the caller’s stack (checkout_token_from: <dir>:<stack>:<key>, read withpulumi config getat the composite’s Pulumi pin, masked; the engine reads it asFACTORY_GITHUB_TOKEN), USER. - T021/T023 (USER 2026-09-17: no new credentials, reuse clanker-infra):
resources.upstash[]gained anexistingform —{ name, existing: { restUrlFrom: secret:NAME | https://…, restTokenFrom: secret:NAME } }, exactly one form per entry (the two are.strict(), a mixed entry is refused by name), the port of CEshared/cache.ts’sexistingRedisUrlshort-circuit. Anexistingentry declares NO Pulumi resource (programLogicalNamesomits it;host downdoes not count it), needs NO management credential (providerEnvsetsUPSTASH_*only for a create entry), the container engine takes the REST pair straight from the two references (no management-API list), andresources destroyREFUSES it (“not ours to destroy”).hosts/dev-host.ymlis nowclanker-redis: existing { secret:REDIS_URL, secret:REDIS_TOKEN }—databaseName: clanker-dev-hostis GONE (so is the D23 “Redis created under the legacy name” clause: the legacy dev-host never owned a Redis; CE’screateCacheshort-circuits onapp:redisUrlanddev-host-container.ymlread staging’s rows), and the dev-host plan holds EIGHT resources, not nine (host-program.test.ts).upstashEmail/upstashApiKeyare no longer referenced anywhere;UPSTASH_EMAIL/UPSTASH_API_KEYleftseed-dev-host.ymland the seed script, which now seedsREDIS_URL/REDIS_TOKENfrom staging. Pre-existing fact, unchanged, now written down: staging, production AND every dev container share that ONE Upstash database (seeding.mdtable).factory validateWARNS on every create-form entry (the management key cannot be verified offline); the create form stays for adopters who own one (docs/byoc.md). - D22 erratum: trunk renamed
develop→main2026-09-17, USER; Replit tracksmain. Every workflow, guard test, hook,factory.yml, skill and doc now namesmain; both Pages projects were re-pointed (production_branch: main);developandfeat/factory-v2are deleted on origin. D22’s “nomain” clause is inverted (nodevelop) — same rule, the other name.gitfwd/**lanes untouched (D14). - T112 §10 erratum (2026-09-17, USER): CE’s release ORDER is
Production — Release(site) FIRST, then the Replit runtime + Workers — ALWAYS, and now ENFORCED. Today’s cutover inverted it (“runtime first”, RETRACTED) because the site build fetched the marketplace catalogue from the livemarketplace.<domain>Worker and got a 522 before the Workers were redeployed. CE now builds/skills/**from a committed snapshot (site/data/marketplace-catalogue.json, refreshed weekly bySite — Refresh Marketplace Catalogueas a PR) and the Replit build’s first commandscripts/assert-site-released.shrefuses a runtime whose commit has no green release (GITHUB_TOKENReplit Secret;CLANKER_SKIP_RELEASE_GATE=1emergency-only). CN’s deploy.yml is one trunk pipeline and has no such split. - T113 (2026-09-17): the two Pages hostnames are attached BY THE DEPLOY PIPELINE, not by an operator or a Pulumi resource —
deploy.ymlpublishgained anattachstep (production only, afterwrangler pages deploy, reusing thecloudflarestep’s outputs because a masked value cannot cross a job boundary): per pair (clankernet-public↔clanker.net,clankernet-app↔app.clanker.net) it deletes ONLY the two known orphans (CNAME -> clanker-public.pages.dev/clanker-app.pages.dev— CE’sclanker.engineerapply of 2026-09-17 forgot them from state but did not delete them; the Pages domains were detached from CE’s projects), errors on any other A/AAAA/CNAME at the name, ensuresCNAME <name> -> <project>.pages.devproxied + commentmanaged-by=clankernet/deploy(the apex is CNAME-flattened by Cloudflare automatically), POSTs the domain to the project, waits ≤3 min foractive(warn-not-fail). The smoke runs its probe sets againsthttps://clanker.net/https://app.clanker.netas well as*.pages.dev, and the “which service serves” step became an ASSERTION once the domain is active (same manifest / same entry bundle as the project URL; Express or another build is::error). No grace period: no production users exist (D25).api.clanker.netis unchanged (host program, T023). Pinned bytests/deploy-workflow.test.ts.
Errata found during implementation (Wave 6 — D24 standardisation, 2026-09-16)
- D24/T023: host id
dev-hostKEPT for now (hosts/dev-host.yml,examples/hosts/dev-host.yml,pulumi/Pulumi.dev-host.yaml, stackwhitehatgg/clanker-infra/dev-host, Tailscale hostnamedev-host), namesclanker-dev-host/clanker-storage-dev-host/ Upstashclanker-dev-host(manifest-facingclanker-redis) unchanged — USER 2026-09-16 revision:development+clanker-*-developmentis the TARGET, a one-line spec edit plus the CE rename in T081 once the legacy dev-host is gone; theclankerlabs-hel1files are deleted. Supersedes D23’s “legacy names” clause only in that the names are now a deferred rename, not a permanent contract.pulumi/Pulumi.yamlisclanker-infra(PULUMI_PROJECTinlayout.ts, the schema default forstate.pulumiCloud.project, and the Automation APIprojectName— a mismatch with the workDir’s Pulumi.yaml is a Pulumi error, so the three are one constant). The stack config key isclanker-infra:host(the engine reads the barehostkey, project-namespaced), notclankernet:host.pulumi configneeds a project directory: the pulumi-config provider now passes--cwd <root>/pulumion every argv (it did not, so the BYOC pulumi-config path could never have run outsidepulumi/);secretsProvider()therefore takesdeps.root. Same-name collision guard:StackHandle.export()+assertStackOwnedinopenHostStack— every verb (up/converge/plan/down/resources destroy AND the container verbs, which open the host stack for its outputs) refuses a checkpoint holding a resource outsideprogramLogicalNames(spec); stack + provider resources ignored; empty passes. Encryption-key trap (not in the plan): Pulumi Cloud keyssecure:values per STACK, sopulumi stack rm whitehatgg/clanker-infra/dev-host(T023 step 3 as written) would delete the key every committed blob is encrypted with — the CE teardown must EMPTY the stack (pulumi destroy), never remove it; if removed,stack init+ re-seed from values captured beforehand (docs/operator/seeding.md§THE TRAP). CE’s legacydev-hostconfig has no committed file:scripts/seed-from-clanker-infra.shrecovers it withpulumi config refreshinto a scratch project (fallback: the four--show-secretsstack outputs), and readsstaging/productionfrom CE’s committed files.deploy.yml/release.ymlread withpulumi stack select … --cwd pulumi+pulumi config get NAME --cwd pulumi(vars.FACTORY_HOST, defaultdev-host;vars.FACTORY_ESC_ENVIRONMENTis gone).tests/no-committed-blobs.test.tsis now CE’s rule (ciphertext allowed underpulumi/; plaintext credential SHAPES refused underpulumi/,hosts/,examples/; every referencedsecret:NAMEis a blob or a row of seeding.md’s table — the table is what the test parses, keep its| \NAME` |shape). The 7 tests red at HEAD180e802(they pinned the pre-whitehatggorgclankerlabs/clankernet/…) are green again through the same edits.app/→client/(git mv, history kept):.dockerignoreallow-list, the gateway Dockerfile/entrypoint/auth-bridge (../../client/src/settings.js),ci-app.yml(jobclient, groupci-client-*),deploy.yml,release.yml,ci.yml, the husky/prettier/eslint ignores,check-lockfile-registry.mjs, four tests; the expo-router directory INSIDE it staysclient/app/. Docker is still unavailable here, so the gateway image build with the new COPY paths is unexecuted (tests/repo-standardsevaluates the.dockerignorerules against both Dockerfiles' COPY sources instead). Nopulumi` binary and no token: nothing was seeded or run against the real account.
Handoff note 5 (2026-09-17 ~15:30) — THE DOMAIN SPLIT IS LIVE
- clankerengineer production is on
clanker.engineer(Pulumi apply green: 4 created / 5 replaced;clanker.engineer,app.clanker.engineer,api.clanker.engineerall 200). No grace period (no users existed);app:legacy*keys removed.main==develop. The site RELEASE failed once because the site build fetched the catalogue from the not-yet-redeployed marketplace worker — being fixed: catalogue SNAPSHOT in the repo + a Replit build gate that REFUSES unlessProduction — Releasesucceeded for the exact sha (USER: release first, runtime after, ENFORCED). Replit deploy of CE is still owed by the operator, after that release is green. - CLANKERNET owns
clanker.net(T113 Pages half done bydeploy.yml: orphans deleted, tagged CNAMEs, domains active;clanker.net+app.clanker.net200). Trunk ismain(renamed from develop; Replit project tracks it); the only other branch is thegitfwd/**lane. Upstash = the existing shared database (REDIS_URL/REDIS_TOKENfrom staging), no management key. Seed lacks only the CN OAuth app pair (T042, owed). api.clanker.netis an orphaned A to Replit’s ingress untilhost upputs the gateway there (temporarygateway.hostis now unnecessary — the product no longer holds the name; fliphosts/dev-host.ymlback toapi.clanker.netbefore the cutover).- This session runs INSIDE a dev container on the legacy dev-host. T023’s
teardown destroys it. T080/T081 are prepared on CE branch
chore/t080-delete-dev-host-provisioning(merge AFTER the teardown ran). The cutover is launched from GitHub (dispatches) — seedocs/operator/cutover-2026-09.md— and a fresh session on the NEW container merges T080/T081 and closes out. - Guard-rail lesson: the production apply needed three one-time inputs
(
allow_dns_deletionreason,preclean_api_records,reset_pages_domainswith--target-dependents) — a zone created by IMPORT carries the registrar’s records, and pre-6.x PagesDomain state cannot migrate. - Owed by the operator: CE Replit deploy (after the release), CN OAuth app (T042), the cutover dispatches, and the Replit cold standby decision (T114b: yes, after the primary is proven).
Handoff note 4 (2026-09-16 ~23:30)
FIRST REAL RUNS. CN: develop deploys green end to end (Pages
clankernet-public / clankernet-app live on their *.pages.dev), release
v1.0.0-rc.1 published factory-base:1 + factory-gateway:1, the dev-host
stack is seeded (10 blobs incl. the host keypair; Upstash/OAuth/release-App
still owed — repo secrets → seed workflow). CE: staging REBUILT FROM NOTHING on
next.clanker.engineer (all jobs green; api-next 200); T112 CODE merged to
develop (6745307a4) — production moves only when the operator runs
PRODUCTION_RELEASE_RUNBOOK.md §10 (store build FIRST). T023 runbook written
(docs/operator/cutover-2026-09.md) with the traps it found (no stack rm;
seed CE env.secrets pre-teardown; keypair minted by the seed; temporary
gateway.host: api-dev-host.clanker.net). Standards: clanker-infra project,
committed config secrets (no ESC), client/, no Replit for CN; dev-host
naming kept (D24 rename deferred). Org secrets: PULUMI_ACCESS_TOKEN (set),
HOSTS_REPO_TOKEN (owed — CE’s caller checks out private CN). GF PR #1 open,
blocked on the whitehatgg account’s Actions billing (user: don’t worry about it).
Fixes from real runs are in the Wave 3/5/6 errata. NEXT: operator steps
(secrets, callbacks, EAS build) → T112 apply → T023 window (T080/T081 inside)
→ T113.
Handoff note 3 (2026-09-16 ~17:00)
Wave 3 COMPLETE and pushed: CN feat/factory-v2 @ 061c46c (T004, T024,
T031, T041 code, T051, site-shell parity; whole tree lint/format/typecheck
clean, 43 suites / 569 tests), CE feat/factory-adoption @ 9547e0737
(T116 + factory.yml D23 fix), GF chore/expo-moved-to-clankernet @ e5d281c
(T090, unmerged). Decisions this session: D22 (trunk + factory-hosted gateway,
no Replit), D23 (full teardown cutover, no adoption), T112 sequence (mobile
build FIRST, 308 grace period bounds T113). Staging (CE) was torn down by the
operator today; the dev-host is Redis-less until T023.
Remaining before the cutover window (T023): T050 (factory-deploy.yml
reusable workflow + setup-factory composite action + CN dogfood caller),
T061 (CE dev image on factory-base, thin caller, .mcp.json), T003
operator items, the operator gates (ESC seed incl. cloudflareAccountId,
Pages projects, GitHub App + v* ruleset, first v1.0.0-rc.1 tag, a Docker
build of both images — none has run on a real daemon yet). Then T023 with
T080/T081 inside it, T082, T091 after GF merges. Domain split T110→T112 is
independent; T113 waits on T023 + T112’s grace period.
Handoff note 2 (2026-09-16 ~14:30)
Wave 2 relaunched as seven parallel agents and COMPLETED: T021(+T002 code
half), T030, T060, T090, T120, T122 — plus the D22 rework (T114 deleted, gateway
replit mode → single-port, one trunk deploy.yml). Every task committed
separately on CN feat/factory-v2, CE feat/factory-adoption, GF
chore/expo-moved-to-clankernet; errata above. NO verifier pass ran on Wave 2
beyond each implementer’s own suite + the orchestrator’s integration run.
Operator-gated leftovers before Wave 3/4: Pulumi ESC env + stack creation and
the seed (docs/operator/seeding.md, add cloudflareAccountId), the two Pages
projects (wrangler pages project create clankernet-{public,app} --production-branch develop), the adopted golden re-capture, a Docker build
of images/base (no daemon on this box). Next: Wave 3 — T031 (needs the
hooks.env allow-list and root .dockerignore from the T060/T030 errata),
T040 follow-ups (publicOrigin on the host Gateway block), T051, T091
(after GF is merged), T116, and CN’s own site-shell.ts + pinned-sha diff test.
Handoff note (2026-09-16 01:50, written by a follow-up session)
The orchestrating session was archived at 01:18 while Wave 2 (wf_ee4da39c-33d) was running; the workflow died with it. State on disk when it stopped:
- Only T021 and T030 implementers had started; NO verifier ran; T060, T114,
T122, T120, T090 never started. Partial T021/T030 output is committed as
WIP
b877f13onfeat/factory-v2(T021: schema extension, host/{cloud-init, converge,layout}.ts, @pulumi deps; missing program.ts, providers, tests. T030: images/{base/Dockerfile,entrypoint.sh,bin/*}; missing README + tests). Re-brief those two to BUILD ON the WIP, not restart. - This plan now lives HERE (
docs/plan/session-plan.md); the old/home/developer/clanker/.local/session_plan.mdis a symlink to it, so the workflow CTX paths still resolve. Edit the file, not the symlink target path. - Branch consolidation: stale local
feat/factorydeleted. GitFWDfix/empty-document-title(2 expo fixes after the vendored pin) was fast-forwarded intochore/expo-moved-to-clankernet, remote branch deleted, and both commits ported intoapp/asa1ad355,bf32aeb(app tests 59/59).feat/gitfwd-coordinationNOT merged, per T082. - All three working branches are pushed: CN
feat/factory-v2, CEfeat/factory-adoption(== develop), GFchore/expo-moved-to-clankernet.