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 state SUPERSEDED 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 owns clanker.net and the net.clanker app identity. Engineers stay on dev-{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 at app.clanker.net (not factory.clanker.net).
  • Landing pages standardised: clankerengineer drops /mobile and /deployed-mode and is branded clanker.engineer; CLANKERNET gets the same-shaped landing under the clanker.net brand 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 Replit SUPERSEDED 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.md is 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:

  1. The ENGINEpackages/engine: a factory CLI (TypeScript, Pulumi Automation API with the provider SDKs as its own dependencies) that turns factory.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, s3 incl. R2 via ?endpoint=).
  2. 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).
  3. The CLIENT — the Expo Router app at app/, identity net.clanker, served on app.clanker.net by 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-cloud under THEIR org, or s3/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”. With pulumi-cloud the 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-outputs test) and never as committed secure: blobs — that is the exact pattern deploy/ENTERPRISE.md:315-316 lists as an open violation for Pulumi.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). The factory/app gateway record is { protect: true }.
  • Authorisation to provision = the caller repo’s own workflow_dispatch permission, optionally tightened by access.github.org or access.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: true on 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-infraclankernet, stack dev-hostclankerlabs-hel1), imported into the new stack, gated on pulumi preview = 0 replace / 0 delete / 0 create. The box, tunnel, Traefik, R2 bucket, Upstash, every dev-*.clanker.net record 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), so docker compose up -d reconciles rather than recreates. Legacy resource NAMES (clanker-dev-host, …) stay. Plan B, only if the gate shows a replace: a second host with dev2-{user} hostnames (isolation verified: legacy reconciler dev-host-server.yml:669-716 matches startsWith('dev-'); orphan sweeps match exact names) — documented, not built.

Decisions

#DecisionResolution
D1CLANKERNET 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)”.
D2Migration path SUPERSEDED by D23~~Adopt the live host’s state (above). Legacy workflows FROZEN (T070) before adoption (T022); legacy stack pulumi stack rm --force after import (state only).
D3State + secrets for the clankerlabs host SUPERSEDED by D24SUPERSEDED 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) under a NEW Pulumi org clankerlabs (so one personal token is not both products’ master key; if an org is not possible now, whitehatgg 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.
D4R2 bucket clanker-storage-dev-hostComes 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).
D5Teardown semanticsteardown.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).
D6Expo 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.
D7feat/gitfwd-coordinationExtract 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.
D8gfp.js couplinggit 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.
D9Nothing else is called factory.ymlTraefik 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.
D10dev-host Pages rehearsal siteDELETE (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.
D11Git credential in containersv1: PAT in the host’s secrets provider (secret:GIT_TOKEN). GitHub App token is v1.1.
D12Hostname template for clankerengineerdev-{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.
D13GFP/1 actor identityv1: 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).
D14CN housekeepingDelete 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.
D15Domain 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.
D16Final 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.
D17CN 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).
D18GitHub OAuth app for the gatewayA 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.
D19SSH key custodyNo 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.
D20SSH authorized keysDefault 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.
D21deploy/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-publicclanker.net, clankernet-appapp.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-hostdevelopment 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/), root package.json with workspaces for packages/* ONLY (never app/).
    • .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 branch gitfwd-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: read on every workflow, DNS gate); workflow-tool-pinning.test.ts — CN copy scans .github/workflows/*.yml AND .github/actions/**/action.yml, vacuity floor >= 1 (CE’s floor > 5 and workflows-only scan, server/__tests__/workflow-tool-pinning.test.ts:30-47, would fail on the single composite pin). Exclude vendor/** from every workflow-scanning test.
    • tests/no-gitfwd-collision.test.ts: no file/dir named gitfwd at root; no workflow triggers on gitfwd/**.
    • Files: /home/developer/clankernet/** (new)
    • Acceptance: npm test green; git for-each-ref refs/heads/gitfwd/ unchanged before/after; the CI filter is asserted by the collision test.

T002: [CN] Pulumi project clankernet, ESC environment, blob-free stack file

  • Blocked By: [T001, T003]
  • Details:
    • pulumi/Pulumi.yaml (name clankernet, runtime nodejs — the engine’s Automation API program uses workDir: pulumi/). Provider SDKs (@pulumi/hcloud, @pulumi/cloudflare 6.19.0 exact, @pulumi/tls, @pulumi/command, @pulumi/random, @upstash/pulumi) go in packages/engine/package.json, pinned to the exact versions in deploy/pulumi/package-lock.json (its package.json pins 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 (from sshPrivateKeyBase64 --show-secrets + derived pubkey), R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY (from devBackends --show-secrets --json), upstashApiKey/upstashEmail (from whitehatgg/clanker-infra/staging config — dev-host uses STAGING’s Redis today, dev-host-container.yml:344-356). NOT cfTunnelSecret — 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: no secure: key anywhere under pulumi/ or hosts/.
    • 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-hel1 decrypts (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 project clankernet; D3 org decision).
    • Tailscale: reuse tag:dev-env for the adopted host and its containers (no ACL change); record in hosts/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.yml sees PULUMI_ACCESS_TOKEN; dig NS clanker.engineer shows Cloudflare nameservers (may land later; T110 waits on it).

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.js minus Expo/i18n rules; app/ keeps its own Expo config) + Prettier (CE’s settings), root scripts lint, 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.ts pins 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.json scripts
    • Acceptance: npm run lint && npm run check:format && npm run typecheck && npm test green; a non-conventional commit message is refused by the hook; git diff --stat of the format commit touches no logic.

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> --squash where <sha> = clanker-gitfwd/.claude/skills/gitfwd/UPSTREAM_SHA (8df1ede…). Write vendor/gitfwd/UPSTREAM_SHA.
    • tests/vendor-drift.test.ts (offline): (1) newest git-subtree-split trailer in git log -- vendor/gitfwd == UPSTREAM_SHA; (2) git diff-tree --quiet <squash-commit>^{tree} HEAD:vendor/gitfwd (no hand edits); (3) sha256 of vendor/gitfwd/clients/web/app/gfp.js + the purity assertions from gfp.test.js:1204-1220 (no require/import/export/Buffer/process/atob/btoa). Optional network re-check behind GITFWD_READ_TOKEN, never the only check.
    • EXPECTED until T091: vendor/gitfwd/clients/expo still exists at this sha (GitFWD keeps it until T090) — app/metro.config.js blockList excludes vendor/** except the clients/web/app watchFolder; CN workflow-scanning tests exclude vendor/**.
    • docs/upgrading-gitfwd.md: git subtree pull + bump UPSTREAM_SHA is 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.sh passes (69 checks).

T011: [CN] Move clients/expoapp/ with history, rewire gfp.js, set identity once

  • Blocked By: [T010]
  • Details:
    • History-preserving extraction (git subtree split --prefix clients/expo in GF → git subtree add --prefix app in CN). Keep package.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.jsmodule.exports = require('../../vendor/gitfwd/clients/web/app/gfp.js'); app/metro.config.js watchFolders: [path.resolve(__dirname, '../vendor/gitfwd/clients/web/app')]; app/e2e/fixture.sh:17ROOT=$(CDPATH= cd -- "$HERE/../../vendor/gitfwd" && pwd); app/e2e/run.js:31const ROOT = path.resolve(APP, '..', 'vendor', 'gitfwd') (ROOT derives from APP=app/, not from e2e/).
    • Identity ONCE (D6, confirmed): app/app.json name CLANKERNET, slug clankernet, scheme net.clanker, ios.bundleIdentifier/android.package net.clanker; replace app/assets/*; src/disclosure.js product name in prose only — HEADER/DETAIL/UNMET substance unchanged (spec §3.6 T-4 / §8.4 C0.10; e2e/run.js imports HEADER).
    • app/README.md: keep the class-CG declaration and single-source rule; replace “not vendored” with the subtree + drift-test contract.
    • CI job app from GF ci.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:web produces dist/index.html; npm run e2e 31 assertions green against vendor/gitfwd; every exported .html carries the CLASS CG header.

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; zod src/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.ts verbs validate | plan | host up|down|converge | container provision|sync|teardown|purge | resources destroy | secrets set | doctor. Required/optional lists grown from deploy/pulumi/shared/dev-host-preflight.ts (drop the -dev-host bucket-suffix rule and assertDevHostAppSubdomain — Clanker-specific; the latter → CE test in T081).
    • host.ref forms (layout section) implemented; stack: form reads only non-secret outputs and the named secrets provider.
    • Schema refusals: ≥3-label hostnames without advancedCertificate: true; ssh-direct without explicit firewall.ssh: true; ingress: none + public: ports → warning; services.postgres.version must be the base image’s shipped major (“image-provided”); access.github needs org or users; hostnames.zone must be in the host’s dns.zones[].
    • schema/factory.schema.json generated (zod-to-json-schema) with an up-to-date test. examples/factory.clankerengineer.yml, examples/factory.minimal-node.yml validate.
    • 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 (or ssh-direct), Pulumi Cloud org + ESC OR s3 bucket + AWS keys + passphrase, a GitHub PAT (read:org if access.github.org, repo for 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/*.yml exits 0; each refusal has a failing fixture; npm run schema:check green; a caller repo containing only factory.yml + hosts/acme.yml validates with no file from CN’s hosts/.

T021: [CN] Host engine: Pulumi program + providers

  • Blocked By: [T002, T020]
  • Details:
    • src/program.ts (Automation API inline program, workDir: pulumi/) ported from deploy/pulumi/index.ts:69-377 + compute/dev-host.ts + shared/{storage,cache,types}.ts. Cloud names {namePrefix}-…; for an adopted host namePrefix = the legacy names. Delete dead inputs letsEncryptEmail/githubPat (compute/dev-host.ts:13,21). Pin traefik and cloudflared image 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: true when adopted), compute/existing-host.ts (customer VM + root key; same converge script), ingress/cloudflare-tunnel.ts (tunnel + catch-all → Traefik :80; factory validate refuses 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 in userData as compute/dev-host.ts:158-159 does today; host-device prune matches host.id EXACTLY + -N suffixes, 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 destroy flips protect and runs the emptier (port of empty-r2-bucket.js).
    • assert-no-dns-deletions ported, run before every HOST up/converge (container records are not Pulumi resources, so the gate never sees them).
    • Tests: no-secret-outputs.test.ts (no pulumi.secret( in outputs; pulumi stack output has 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-hel1 against the real account previews as in T022; for a FRESH host spec every hcloud/tunnel/upstash step’s inputs.name starts with the spec’s namePrefix; no-secret-outputs green; pulumi stack output shows only serverIp, 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):
    1. Announce + freeze. Engineers push every branch; gitfwd lanes are on GitHub, nothing to save. Snapshot for the record only: hcloud server list, pulumi stack export of whitehatgg/clanker-infra/dev-host to the operator’s machine, wrangler r2 object list clanker-storage-dev-host | wc -l (so ‘we knew what we deleted’ is on file).
    2. 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 as REDIS_URL/REDIS_TOKEN from staging’s app:redisUrl/app:redisToken instead; PLUS cloudflareAccountId), and create the two Pages projects. Verify factory plan host clankerlabs-hel1 renders against the real account with 0 errors while the legacy host still exists (name collisions on clanker-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).
    3. 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=teardown with 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 NOT pulumi stack rm it — CN reuses this exact stack name, and Pulumi Cloud keys the committed secure: blobs to the stack; removing it drops the key. The CE teardown’s pulumi destroy leaves it EMPTY, which the engine’s ownership guard accepts. Tailscale: tailnet-prune.sh for clanker-dev-host and every dev-* device.
    4. CN factory host up clankerlabs-hel1 (with gateway.host set to a TEMPORARY name under clanker.net — the product’s api.clanker.net is still live; see Wave 3 errata) → host converge. Gate: pulumi stack output shows 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-deletions ran.
    5. Re-provision every engineer via factory-deploy.yml action=provision (CE caller, T061 image) — fresh volumes, db:migrate + builtins:push run by the hooks; each https://dev-<user>.clanker.net/api/health/ready 200; Tailscale ssh works; wrangler dev --env dev-host --remote binds the new bucket.
    6. Delete in CE (T080/T081 executed inside the window, not later): both dev-host workflows, deploy/pulumi dev-host modules, dev-entrypoint.sh, Dockerfile.dev legacy path, the two workflow tests, deployment/SKILL.md dev-host sections, rule 11 → ‘dev containers come from CLANKERNET’. Merge CE feat/factory-adoption → develop.
    • Files: docs/operator/cutover-2026-09.md (CN), CE deletions per T080/T081
    • Acceptance: whitehatgg/clanker-infra/dev-host holds only CN’s resources (pulumi stack export | jq '.deployment.resources|length'); hcloud server list shows ONE server, created in the window; every engineer’s container answers on its old hostname; grep -rn "dev-host" .github/workflows deploy/pulumi in CE → none; CN npm test + CE npx jest green; the operator doc’s every command was the one actually run (edited in place during the window).

T024: [CN] Delete the adoption machinery from the engine (D23)

  • Blocked By: [T021]
  • Details: remove adopted from schema/host.ts and every branch on it (ignoreChanges: ["userData"|"tunnelSecret"], protect flips tied to adoption, frozen legacy logical names — names become {namePrefix}-… unconditionally, with namePrefix: clanker-dev-host kept in hosts/clankerlabs-hel1.yml so the fresh resources carry the legacy NAMES without legacy STATE); delete packages/engine/scripts/rewrite-checkpoint.ts, test/adopted-host.golden.test.ts + fixture, any adoption prose in docs/hosts.md/docs/byoc.md/seeding.md (the seed list loses hostSshPrivateKey/hostSshPublicKey/cfTunnelSecret; a new keypair is generated on the runner before the first up and stored in the secrets provider by host up itself). existing-host stays. 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-outputs green; factory plan host clankerlabs-hel1 (mocked) creates the server, key, firewall, tunnel, bucket, Redis with clanker-dev-host/clanker-storage-dev-host/clanker-redis names.

P3 — Engine: containers

T030: [CN] factory-base image + generic entrypoint

  • Blocked By: [T010, T020]
  • Details:
    • images/base/Dockerfile from the generic layers of deploy/Dockerfile.dev:19-111 PLUS postgresql-15 postgresql-client-15 (Dockerfile.dev installs postgres at :124 in the “repo layer” — it is generic) and run-local-ci (inert without sysbox): node:22-slim, sshd with an AuthorizedKeysCommand script that reads its SOURCE from env (github-keys default, sshid opt-in — D20), tailscale, docker-ce client, mosh (NEW — today only the firewall opens its ports; features.mosh gates it), tmux/overmind, claude-code/codex/tsx, developer user; vendor/gitfwd/{bin,mcp,hooks,skills}/opt/factory/gitfwd; ENTRYPOINT ["/opt/factory/entrypoint.sh"].
    • images/entrypoint.sh = the GENERIC skeleton of deploy/scripts/dev-entrypoint.sh: tailscale join (pin --stateful-filtering=false and --accept-dns=false in a golden test), sshd, clone repo.path with env.git, local Postgres iff services.postgres (FATAL on failure), write env.file, run hooks.postClone then hooks.provision, GFP/1 setup ONLY when coordination: is set (clone, mint ONCE — never re-init a clone with gitfwd.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.json from coordination.{space,cloneDir} when absent; export GITFWD_HOME, GITFWD_REPO, PATH), tmux windows from services.procfile.
    • Installs /usr/local/bin/factory-start (Procfile → tmux), factory-sync (ff sync.branch, run hooks.sync, restart sync.restart windows), factory-auto-sync (loop on sync.autoSyncSeconds) — replacing dev-entrypoint.sh:595-846’s start-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.yml boots to tmux with Postgres running and env | grep -c GITFWD == 0; the branch’s gitfwd-factory.test.ts “dev container bootstrap” cases re-targeted at images/entrypoint.sh are green; docker inspect Entrypoint == /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-1193 into src/container/*: identity (actor; access.github.org via a PAT from the host secrets — GITHUB_TOKEN cannot read org membership; users list needs no PAT), env resolution (static ∪ templated ∪ secrets ∪ computed ∪ reserved), generated docker-compose.<project>.yml (environment from the env list; project dev-{user} when hostnames.default is dev-{user}.{zone}, else {user}-{repo}; volume keys exactly repo-data, postgres-data, tailscale-state, ssh-keys, docker-data — the ADOPTION CONTRACT, test-pinned), Traefik file-provider routes from ports[] + hostnames (from deploy/bin/generate-port-config.sh), env file 0600 root on the host, docker compose up -d, SSHID_USERNAME from access.ssh.users override (default = login).
    • src/container/verify.ts (the checks that turn a silently-dead env into a failed run, from dev-host-container.yml:642-649,780-870): Traefik Host-header probe on the host (404/000 after 60s = fail); Tailscale join + sshd probe via docker exec tailscale status; public probe https://{hostnames.default}{ports[default].health} through the tunnel; features.innerDocker → probe host runtimes, set runtime: sysbox-runc or WARN (never fail).
    • DNS: idempotent tagged API writes (create/PUT/DELETE only records with comment: managed-by=factory/{host}); UNTAGGED legacy dev-{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 delete dev-alice when removing dev-al.
    • sync: ff sync.branch, hooks.sync, restart windows, refresh sshid/github keys, restart Tailscale (:960-1062). teardown honours preserveVolumes; purge removes volumes; both remove tagged DNS + the exact Tailscale device. Onboarding card → job summary ONLY (D13).
    • Concurrency key container:{host}:{user}:{repo}. Port CE’s dev-host-container-workflow.test.ts assertions as behavioural tests over generated output; behavioural test for the Tailscale matcher with devices dev-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> → reachable https://<you>-api.<zone> + ssh developer@<hostname> over Tailscale; teardown keeps volumes, purge removes them; a manifest whose default port has no listener FAILS with a Traefik-404 error; on a throwaway dev-<tester> project created with the LEGACY compose, provision reports Recreating (not Creating) and the volumes’ CreatedAt are unchanged.

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.ymlhost/traefik.gateway.yml (D9).
    • Parameterise auth-bridge.js: org, PUBLIC_ORIGIN, space, OAuth client from env; import KEYS from app/src/settings.js (module.exports = { load, save, KEYS, DEFAULTS }), pinned by one test. Scopes read:user read:org now.
    • 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. Image ghcr.io/clankerlabs/factory-gateway with app/dist baked at release — removes boot-time npm ci and the GitFWD/CLANKERNET clone PAT from customer hosts (a GHCR pull token is still needed while packages are private — T051). --app only when dist/index.html exists (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-factory tests 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/node and the app through front.js on one port.

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 in host/traefik.gateway.yml, DNS record { protect: true } in T021’s host program). gw-hel1.clanker.net is dropped as a hostname; the same service answers api.clanker.net after T113 and the tunnel URL before. Rollout via factory-deploy.yml (T050) from CN’s own root factory.yml; rollback = previous image tag.
    • host converge deploys docker-compose.gateway.yml only when the host spec has gateway:; secrets from the host’s secrets provider. clankerlabs PRODUCTION is this service (D22): the gateway: block on clankerlabs-hel1 answers api.clanker.net (tunnel hostname until T113), with a weekly dogfood.yml smoke (/gf/v1/node 200 + 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/node 200 via the tunnel; login works with the dogfood OAuth app; weekly smoke green.

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/* to api.clanker.net). The Replit secret list is gone; GITHUB_CLIENT_ID/SECRET, GITFWD_BRIDGE_SECRET, GIT_TOKEN, COORDINATION_REPO_URL, PUBLIC_ORIGIN are host secrets in ESC consumed by the gateway service’s env.secrets.
    • Org-owned OAuth app CLANKERNET — callback https://app.clanker.net/auth/github/callback (unusable until T113; that is expected), scopes read: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 on app-next… unaffected; the product’s OAuth app lists no factory.*/app.clanker.net callback.

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 ci in packages/engine, exports FACTORY_ROOT=${{ github.action_path }}/../../...
    • .github/workflows/factory-deploy.yml (workflow_call): inputs action (host-up|host-down|host-converge|provision|sync|teardown|purge|resources-destroy), host, manifest (default factory.yml), ref; secrets PULUMI_ACCESS_TOKEN (optional), FACTORY_PASSPHRASE (optional), AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (optional), FACTORY_SECRETS (optional JSON name→value — the factory-secrets provider; a workflow_call has no open-ended secrets map, so github-environment as a provider is dropped from v1), HOSTS_REPO_TOKEN (optional). permissions: contents: read, packages: read (no id-token: write until OIDC lands in T102). Concurrency host:{host} / container:{host}:{actor}:{repo}. Steps: checkout caller at ref → setup-factory → factory validateenv.computed → login backend → verb → no-secret-outputs gate → summary.
    • dogfood.yml: workflow_dispatch caller using ./.github/workflows/factory-deploy.yml (local ref, so it tests the tree under change) with a factory_ref input for the composite; runs CN’s own factory.yml ({user}-clankernet.clanker.net, image.base only, services.postgres off, coordination: on); weekly gateway smoke (T041).
    • tests/sha-pinned-uses.test.ts: every uses: inside factory-deploy.yml and the composite is SHA-pinned. docs/quickstart.md recommends @<40-char sha> + Dependabot github-actions for adopters; @v1 only 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=provision provisions a CN container on clankerlabs-hel1 with PULUMI_ACCESS_TOKEN as 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 + push ghcr.io/clankerlabs/factory-base:<tag>,:1 and factory-gateway:<tag> with actions/attest-build-provenance + SBOM (ENTERPRISE.md §1.2b Tier 0, re-homed per D21); publish schema/factory.schema.json; move the v1 and v1.x tags to the release commit (release.yml is the ONLY mover; tag protection on v*); release notes carry the workflow/action SHA. First tag v1.0.0-rc.1 (which also creates v1).
    • Packages PRIVATE until T103: grant repo clankerengineer Actions access and the service-account user (app:githubToken, staging-deploy.yml:190) read on both packages; every adopter host needs image.pull.tokenFrom (GHCR read:packages) — recorded in docs/byoc.md as a v1 limitation.
    • Files: .github/workflows/release.yml, docs/releasing.md
    • Acceptance: git ls-remote origin refs/tags/v1 → latest v1.x commit; a direct git push origin v1 is refused; CE build-dev-image pulls FROM factory-base:1 with the existing PAT; attestations visible on both packages.

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.yml from examples/factory.clankerengineer.yml: ports from container.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, exports DATABASE_URL/DIRECT_URL/TEST_DATABASE_URL), procfile: Procfile.dev, resources.r2 = clanker-storage-dev-host (adopted), resources.upstash = the adopted clanker-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_URL from {port:name}, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AI_GATEWAY_URL), env.secrets (the ~17 names + KMS_MASTER_KEYgen-worker-dev-vars.sh:73 needs it), env.computedscripts/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.sh FIRST (carries dev-entrypoint.sh:92-118 and :347-357: derived KMS_MASTER_KEY, EXECUTOR_AUTH_TOKEN fallback, 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.postClone installs scripts/factory/bin/{restart-server,wait-for-server} on PATH, hooks.sync (db:migrate), access.ssh.keySource: sshid, agents.claude.mcp expo, features.innerDocker/mosh, coordination (CLANKERNET, core, dev-{user}, /home/developer/coordination), teardown.preserveVolumes: true, hostnames dev-{user}.{zone} / {port}-dev-{user}.{zone} on the host’s default zone clanker.net (no hostnames.zone override — engineers stay put). check-ci: keep via .claude/commands/check-ci.md only; parameterise check-ci.sh:7/:104 from repo.name.
    • scripts/factory/env-keyset-diff.sh: devBackends (index.ts:249-372) ∪ compose environment: ∪ heredoc, MINUS an explicit dropped: list with a reason per key (dead passthroughs docker-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 a container-derived: list (postgres URLs, SESSION_SECRET, EXPO_PACKAGER_PROXY_URL, EXPO_PUBLIC_APP_URL, EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY, CLANKER_SKIP_BOOT_MIGRATE), must equal static ∪ templated ∪ secrets ∪ computed ∪ reserved; ALSO every VAR= assignment in dev-entrypoint.sh outside 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.yml exits 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, beside Dockerfile.dev which stays untouched until T080): FROM ghcr.io/clankerlabs/factory-base:1; ONLY the repo layer (server_dist, node_modules, playwright, python venv, Procfile.dev); NO COPY dev-entrypoint.sh, NO COPY container.json, NO ENTRYPOINT (inherited), no duplicate postgres apt; keep .npmrc on every COPY … package-lock.json (npmrc-skips-onnxruntime-download.test.ts:48-63). staging-deploy.yml build-dev-image (:137-251) gains a second target → ghcr.io/clankerlabs/clankerengineer:factory; factory.yml image.ref points at :factory. :dev keeps 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), explicit secrets: { 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 at cloneDir, remote origin, 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.ts green with old + new workflows; line-by-line audit: every line of dev-entrypoint.sh maps to images/entrypoint.sh, a factory-* 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.ts lines 69-377 (if (cloud === "dev-host")} else { at :378), imports :10,:12 (preflight) and :25 (createDevHost), the environment === "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 (rename Dockerfile.factoryDockerfile.dev, retire the :dev tag in staging-deploy.yml); .dockerignore:104-120 negations 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 of bootstrap-pulumi-stack.sh (:123,159,260-300,399) and sync-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; split dev-host-parity.test.ts (app-contract half → factory.yml: ports, Procfile bind addresses, wrangler remote = true, INNGEST_DEV; provisioning half deleted); adjust workflow-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 jest green; 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 runtime APP_ENV=dev-host branches.

T081: [CE] Rewrite docs, skills, rule 11; relocate the app-subdomain guard

  • Blocked By: [T080]
  • Details:
    • Added (D24): rename the env identifier dev-hostdevelopment across 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 binding clanker-storage-development, gen-worker-dev-vars.sh, Pulumi.dev-host.example.yaml → deleted with the stack, factory.yml env.static.APP_ENV=development, every test naming dev-host. grep -rn dev-host → only history/CHANGELOG.
    • CLAUDE.md: /deploy block, Deployment table (dev-host → “provisioned by CLANKERNET from factory.yml”; drop the factory.clanker.net row), Rule 11 → “Dev containers are provisioned by CLANKERNET’s reusable workflow from factory.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 by provider-profile/SKILL.md:607), e2e/helpers/global-setup.ts:36 comment.
    • Skills: deployment-ops (SKILL + references/dev-host-containers.md → pointer + Clanker-runtime sections; references/secrets-management.md key 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,416 comments, base-layer, feature-skill-execution, workflow-system, provider-profile, sandbox-execution, scaffold-deploy-ci, .claude/agents/deployer.md; memory notes dev-host-pulumi-ci.md / pulumi-org-decryption.md corrected (root cause = gitignored stack file) and the Tailscale/sysbox notes moved to CN docs/operator/.
    • STAYS (document as staying): APP_ENV=dev-host runtime 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.
    • assertDevHostAppSubdomainserver/__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 jest green; Skills Reference groupings still true.

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/bin binaries).

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-652 guard → “no package.json anywhere”; delete the expo job :618-690 and clients/expo entries in :120-135; drop .gitignore:14-20; edit README.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.yml green with zero package.json in the tree; release.yml unaffected.

T091: [CN] Bump UPSTREAM_SHA past the removal

  • Blocked By: [T090]
  • Details: git subtree pull --prefix vendor/gitfwd … <new sha> --squash; update UPSTREAM_SHA; tests/vendor-shape.test.ts asserts vendor/gitfwd/clients/expo is absent and vendor/ holds no package.json/lockfile; re-run drift + app e2e.
  • Acceptance: ls vendor/gitfwd/clientstui web; drift green; cd app && npm run e2e green; 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.json artifact) and dispatches provision/sync/teardown with the USER’s OAuth token (repo/workflow scopes added to the CN OAuth app); factory doctor reports caller-pin / host / base-image skew.

T101: [CN] One GFP/1 actor per human

  • Blocked By: [T071]
  • Details: coordination.identity: gateway; identity map persisted into refs/heads/gitfwd/meta. Optional factory actor 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-direct ingress, GitHub App token for env.git, existing-host end-to-end test, gitfwd-runner start-up, archive-tag ssh2 gateway evaluation.

T103: [CN+CE] Split coordination lanes; go public

  • Blocked By: [T071, T002]
  • Details: new private clankerlabs/coordination; coordination.repo in CE + CN dogfood + Replit GIT_TOKEN scope → the new repo; the existing e2e-verify-* lane in CLANKERNET is never deleted/force-pushed. Precondition already met by D3: no secure: 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.engineer as a Pulumi resource in CE’s deploy/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 to clanker.net only — dev containers do not use this zone.
    • Files: deploy/pulumi/shared/dns.ts (+ zone), docs/operator/zones.md (CN, records that clanker.engineer is the product’s, not the factory’s)
    • Acceptance: curl https://probe.clanker.engineer (throwaway proxied record) returns a Cloudflare cert; zone id recorded in Pulumi.staging.yaml.

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.ts custom domains (next, app-next); assert-no-dns-deletions expectations; wrangler -next routes + zone_name (workers/*/wrangler.toml:76,:81,:197); ensure-workers.sh; e2e-staging.sh, smoke-static-site.mjs, smoke-dynamic-judgement.mjs; tests static-site-hostnames, worker-host-derivation, production-stack-shape; eas.json preview; trustedOrigins; OAuth callbacks (GitHub app, Google, Apple services id) for app-next.clanker.engineer; Stripe test webhook; Nango callback; .well-known/*, llms.txt, openapi.yaml; 52 doc/skill mentions of next.clanker.net. Keep *-next.clanker.net as 301s for one release.
  • Acceptance: staging-deploy.yml green end-to-end on next.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; Pages clanker-publicclanker.engineer; the Expo web app → app.clanker.engineer (executing HOSTNAME_SPLIT_PLAN.md’s marketing/app split on the new zone — the APP_ROUTES regex coupling in scripts/build-static-site.ts is deleted, not ported); production Pulumi stack + production-apply-infrastructure.yml; wrangler production routes; eas.json production EXPO_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 under engineer.clanker but app.json:7,11 still says net.clanker — correct the repo to match the deployed identity (scheme, ios.bundleIdentifier, android.package = engineer.clanker; CFBundleDisplayName unchanged); verify against the EAS project (f9fca744…) and store records before editing; deep-link scheme ripples into client/App.tsx linking + maestro flows. clanker.net → 301 shim in the existing Pages _worker.js until T113; API clients get Location + 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, Pages clanker-public and the AI Gateway are hostname-agnostic and stay. Only NAMES move — app:domain, the 7 DnsRecords, the PagesDomain, worker routes, the Replit custom domain — and Pulumi moves them as REPLACEs (record name is ForceNew) that the DNS gate refuses until ALLOW_DNS_DELETION="<reason>" is passed deliberately. Order:
      1. Zone ACTIVE + Universal SSL on clanker.engineer (T110). Start the NS change today; up to 48h.
      2. Register the new callbacks ADDITIVELY where the provider allows several: Google, Apple Sign-In return URLs (services id engineer.clanker is already right — Pulumi.production.yaml app:appleClientId), a SECOND Stripe live webhook endpoint at https://api.clanker.engineer/api/v1/billing/webhook. GitHub’s OAuth app holds ONE callback → register a new product app for app.clanker.engineer; its client id replaces app:githubClientId at step 4.
      3. Ship a store/EAS build pointed at api.clanker.engineer BEFORE the flip. EXPO_PUBLIC_DOMAIN/EXPO_PUBLIC_APP_URL are baked at build time; every installed app talks to api.clanker.net and breaks the instant that name stops answering. This is the constraint the old brief missed.
      4. Flip: app:domain: clanker.engineer (+ zone id) in Pulumi.production.yamlProduction — Apply Infrastructure with the DNS override → worker routes*.clanker.engineer in all three wrangler.toml + redeploy → Replit: link api.clanker.engineer, new app:managedOriginVerifyTxt → merge main so Production — Release publishes the site → operator Replit redeploy. Downtime = DNS propagation.
      5. Grace period on clanker.net: api.clanker.net answers a 308 (not 301 — a 301 turns every POST into a GET) to api.clanker.engineer, apex + app. get a Cloudflare redirect rule to clanker.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 take api.clanker.net for its gateway until it ends.
      6. Remove the redirects; T113.
    • HOSTNAME_SPLIT_PLAN.md does 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.
  • Acceptance: Production — Release green on clanker.engineer; Replit redeploy serves on the new API host; curl -I https://clanker.net/ → 301; a fresh EAS preview build opens engineer.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.net and app.clanker.net are attached AUTOMATICALLY by deploy.yml’s attach step on every production publish (no operator action, no Pulumi resource, no grace period — no production users exist, D25 context). api.clanker.net remains the host program’s gateway record (T023/T041) and still waits on T112’s grace period; hosts/dev-host.yml keeps 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 → Pages clankernet-public (T121, CN deploy.yml); app.clanker.net → Pages clankernet-app (T122); api.clanker.net → the host tunnel record for the gateway service (T041, D22) — PUBLIC_ORIGIN=https://app.clanker.net in the gateway’s host secrets. DNS records as CN Pulumi resources with ALLOW_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.net records STAY (factory containers, tagged); dev-host.clanker.net gone by T115.
    • Amended (D22): attaches clanker.netclankernet-public, app.clanker.netclankernet-app (Pages custom domains) and api.clanker.net → the host tunnel (a host DNS record, T021/T041). No next./app-next. names, no Replit. Until then the *.pages.dev URLs + the tunnel hostname.
  • Acceptance: https://clanker.net serves the CLANKERNET landing; https://app.clanker.net serves the app, GET https://app.clanker.net/gf/v1/node 200 through the proxy, GitHub login completes on the app origin; curl https://api.clanker.net/gf/v1/node 200 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.ts static pins, README prose) is reverted; the entrypoint’s replit mode is renamed single-port or removed (T040 follow-up, same session). .replit must 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 .replit conventions (comments per non-obvious line; no [agent] integrations; no phantom [workflows]).
    • entrypoint.sh Replit mode: STATE_DIR=${REPLIT_STATE_DIR:-/srv/gitfwd}; clone the coordination repo with GIT_TOKEN at boot; gateway --listen 127.0.0.1:8470 --behind-tls-proxy + bridge :8471 behind front.js on 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’s redirect_uri is on the APP origin, whose worker proxies /auth/* here). replit.md restored + rewritten. No custom domain until T113 — verify on the *.replit.app URL (login cannot complete there; that is expected — gw-hel1 covers it). The gateway must accept the proxied Host/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/node 200; SSE stays open >5 min; a redeploy loses no message (gitfwd read count before == after); login is verified end-to-end at T113.

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) and docs/releasing.md (T051) carry what survives (§3.2/§5.1b trust model, §7.2 shared responsibility, §1.2b provenance); the /deployed tier 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 its no-secret-outputs sibling 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,297 keeps its property (no vendor call-home) with the doc pointer rewritten to CN docs/byoc.md.
    • The byoc DeploymentEnv is dead code without a BYOC product: remove server/infra/profile.ts:277 (and the byoc member of DEPLOYMENT_ENVS), the environment !== "byoc" guards deploy/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" across server/ 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:types green.

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 /mobile link (:1084,1112,1175,1457,1475), the Deployed pricing card :1437 and 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.html and docs/concepts/workspaces.md lose 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.js prefixes, site/README.md, deploy/STATIC_SITE.md; tests landing-install-snippet, base-layer-wire, no-client-llm-sdk, workflow-base-layer-resolve where they enumerate pages.
    • Files: as listed
    • Acceptance: static build green; check-static-site.mjs passes 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); no deployed-mode string in site/.

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 to develop, 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 from home.html; copy deployed-mode.html NOW. Pages: / (“CLANKERNET — Deploy Your Own Clanking Machine”: what a factory is, factory.yml in 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, linking docs/byoc.md), /docs/ (from CN docs/), /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.json shape, check-static-site pattern. CE’s site/README.md links it. A CN test diffs the shell against CE’s site-shell.ts at a pinned CE sha (once T120 lands).
    • .github/workflows/site-deploy.yml (CN): builds site/ and publishes Pages project clankernet-public (wrangler pages deploy, Cloudflare token from ESC via PULUMI_ACCESS_TOKEN), branch-filtered; custom domain clanker.net attached at T113 — until then the *.pages.dev URL.
    • Files: CN site/**, docs/site-standard.md, CN ci.yml (build + check job)
    • Acceptance: CN site builds; copied check-static-site.mjs passes; the tier page makes no claim docs/byoc.md contradicts.

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.yml with ONE .github/workflows/deploy.yml: on: push: branches: [develop] builds site/ + app/ and publishes clankernet-public (→ clanker.net) and clankernet-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_request publishes both as PREVIEW deployments (--branch) and writes the URLs to the job summary. No environment: 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 exported index.html for app routes; proxies /gf/v1/* and /auth/* to https://api.clanker.net (configurable via a Pages env var GATEWAY_ORIGIN), streaming SSE unbuffered and forwarding Authorization; the same shape as CE’s app-site worker in HOSTNAME_SPLIT_PLAN.md (“the app Pages project keeps a same-origin /api + /auth proxy”). Expo asset misses → 404, never the shell (the CE fallthrough lesson).
    • app/src/settings.js default gateway stays window.location.origin on 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 ci in app/, expo export --platform web, copy _worker.js + _headers into dist/, wrangler pages deploy to project clankernet-app; branch-filtered, permissions: contents: read, concurrency; custom domain attached at T113.
    • tests/app-worker.test.ts: the worker routes /gf/v1/spaces/core/events to 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.dev URL serves the app; with GATEWAY_ORIGIN pointed at the *.replit.app deployment, GET <pages>/gf/v1/node 200; SSE frames arrive through the proxy.

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/gitfwd never lists it; the test finds it via git log --grep=git-subtree-split. diff-tree against HEAD:vendor/gitfwd differs by exactly A UPSTREAM_SHA (the pin lives inside the prefix) — asserted as the ONLY difference. CI checkout needs fetch-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 for vendor/.
  • T001: CE’s real ts-jest is 29.4.12 (the 29.4.6 string is a stale entry); jest kept at ~29.7.0. .gitfwd/node.json was 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 with uses: ./.github/actions/setup-factory (one pin site).
  • T020: zod 4 has built-in z.toJSONSchema (no zod-to-json-schema). The stack: host.ref form needs a non-secret hostSpec output — T021’s allow-list is serverIp, tunnelId, tailscaleHostname, hostSpec. container.json marks 8081 public: false but it is path-routed on the public host; schema public = “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 them fatal: false. STATIC_SITE_URLhttps://{hostname} (the rehearsal site is deleted, D10). npx factory on the public registry is an unrelated package — docs say node packages/engine/bin/factory.js.
  • T040: two listeners cannot share 8470 — front.js owns ${PORT:-8470}, gateway/bridge listen on 18470/18471 in Replit mode. The branch spliced GIT_TOKEN into 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.sh needs bash (re-execs itself under sh). The “dev container bootstrap” cases of gitfwd-factory.test.ts belong to T030, not T040. GATEWAY_MODE=replit is the explicit switch T114 sets.
  • T121: CE’s site/ has no package.json/lockfile (astro lives in CE’s root) — CN site/ 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.ts cannot use import.meta.url under Astro (bundled into dist/). site/public/assets/site-chrome.js:3 carries 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). .dockerignore is an ALLOW-LIST: packages/gateway-service/Dockerfile also builds from the repo root and COPYs app/src/settings.js + app/dist, so app/ cannot be excluded wholesale; tests/repo-standards evaluates it with moby’s matcher. deploy.yml gates on a mode output computed in build (not a bare job-level if) — CLAUDE.md states the real mechanism. typescript-eslint 8.70 needs @eslint/js on the eslint major (9.39.1). app/ has no lint script → not in root lint (pinned; flip when it gains one). Stale under D22/D23 and still to fix: docs/operator/seeding.md lists the SSH-key/tunnel-secret seed items (T024 drops them); README.md says the gateway is served at factory.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/pulumi pinned 3.253.0 (CE lock has 3.223.0; the CLI pin wins); other SDKs match CE’s lock exactly. The assert-no-dns-deletions port drops the app-site-dns/81058 pair (a Clanker Pages resource this program never declares). empty-r2-bucket.js had 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: cfTunnelSecret is not seeded, so the program passes no tunnelSecret + ignoreChanges: ["tunnelSecret"]; no RandomBytes on adopted hosts (0-create holds). access/tailscale.ts is runner-only, not a Pulumi provider. compute.firewall.ssh doc is misleading: 22 is always open (converge runs over it); ssh: true is only the ssh-direct acknowledgement. Pulumi setMocks hides resource options — test/helpers/pulumi-mock.ts wraps the monitor to assert protect/ignoreChanges. hosts/clankerlabs-hel1.yml (T003’s file) created here. Not done here: no pulumi CLI / token — ESC env + stack not created, nothing seeded, real-account factory plan host NOT run (steps in docs/operator/seeding.md, which must also gain cloudflareAccountId per the T122 erratum); the adopted golden carries a PLACEHOLDER public key and must be re-captured from legacy.json (UPDATE_GOLDEN=1) before the T022 gate. docs/hosts.md cites dns-ownership.md (T031, not yet written).
  • T060: CE a6ef16a6b. check-ci.sh lives at deploy/bin/check-ci.sh (not scripts/), parameterised from repo.url (repo.name lacks the owner). container-derived shrank to ONE key: EXPO_PACKAGER_PROXY_URL/EXPO_PUBLIC_APP_URL are https://{hostname} templates, postgres URLs are services.postgres.exports, SESSION_SECRET and CLANKER_SKIP_BOOT_MIGRATE are DROPPED (no reader / always empty), DEV_BASE_DOMAIN dropped too. EXECUTOR_AUTH_TOKEN fallback is at dev-entrypoint.sh:307 (inside the heredoc); compose dead passthroughs at :103-117. The /app/node_modules symlink (dev-entrypoint.sh:379-383) was missing from the CN example — now a postClone hook; T061’s Dockerfile.factory must keep node_modules at /app. Engine gap (T030/T031): hooks run sudo -u with only the env file, so RESERVED GIT_TOKEN is invisible to gen-worker-dev-vars.sh:56 (writes it as the marketplace worker’s GITHUB_TOKEN) — needs hooks.env: [GIT_TOKEN] or the credential helper exposed to hooks. host.ref: repo:clankerlabs/CLANKERNET@v1:hosts/clankerlabs-hel1.yml resolves nowhere until hosts/ exists at CN root (T021) and v1 is tagged (T051). Adopted clanker-owned databases go through the entrypoint’s ALTER/REASSIGN path (base image role is factory). sync.autoSyncSeconds: 300 vs legacy 30s — deliberate, flag if 30 wanted.
  • T122 (D22): on: push: branches: [develop] is IMPOSSIBLE beside D14’s branches-ignore: ['gitfwd/**'] (GitHub forbids both on one event; no-gitfwd-collision.test.ts refuses the allow-list form) — deploy.yml uses branches-ignore + a job-level if: github.ref == 'refs/heads/develop'. ci.yml/ci-app.yml PR triggers narrowed to [develop] (no main). cloudflareAccountId (non-secret) must be in the ESC seed beside cloudflareApiKey (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 (and clankernet-app); the workflow asserts, never creates. GATEWAY_ORIGIN default already https://api.clanker.net — nothing to set. /gf/v1/node smoke warning-only until T113. site/scripts/static-site-env.mjs still accepts STATIC_SITE_ENV=staging (dead under D22). expo export web.output: static emits per-route HTML; /assets/* is a fourth guarded miss shape.
  • T120: CE 241645cb7. The generated _worker.js never enumerated pages (“prefixes” had nothing to edit) — a RETIRED_PAGES list now answers /mobile, /deployed-mode with a plain 404 (a 301 to /#get-started / clanker.net/deployed is an option after T113). /mobile had consumers outside site/ 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, the feature-auth skill. Only landing-install-snippet enumerated pages; the other three named suites say “mobile” about the client. site-chrome.js source is assets/ (copied into site/public/assets at build) — site-standard.md says otherwise, fix on the CN side. CE has the FIRST site-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:31 hardcodes the https://clanker.net docs canonical (T112).
  • T121 follow-up (CN site-shell.ts + parity test): CE 241645cb7 is the pin (tests/fixtures/site-shell.ce.sha; the file is unchanged at CE HEAD a6ef16a6b). The parity slice is the RENDER half only — from the first export to the // Brand data banner — because CE’s header comment says “clanker.engineer (this repo)” and cannot be copied verbatim. CE’s renderFooter ignores NavLink.attrs, so the footer “The app” link lost its data-app-cta (the nav’s keeps it; check-static-site reads index.html’s and passes) — fix on the CE side (renderExternal) and move the pin. NAV/FOOTER presets carry the literal APP_ORIGIN token, so render-html-page.ts substitutes 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.md claimed scripts/check-static-site.mjs and site-chrome.js live under site/ on both sides — in CE both are ROOT (scripts/, assets/); fixed. STATIC_SITE_ENV=staging dropped (production | preview, D22). The landing footer’s dead /docs/quickstart link became /docs/ “Overview”. tests/fixtures/ is now excluded from eslint + prettier (byte-pinned golden, same rule as packages/*/test/fixtures/). site/public/assets/site-chrome.js re-synced byte-for-byte from CE assets/site-chrome.js (T120 fixed the :3 path comment there).
  • T114 (deleted, D22): the T040 replit mode survives as single-port — it is what the LOCAL packages/gateway-service/docker-compose.yml and the “Testing without Docker” recipe run (one port, no Traefik); production host/docker-compose.gateway.yml runs GATEWAY_MODE: docker and never front.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’s Gateway block needs a publicOrigin field (or an ESC secret). COORDINATION_REPO_URL is a required gateway secret with no default.
  • T090: GF had TWO dependency guards, not one — the gateway job’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 GF e5d281c on chore/expo-moved-to-clankernet (unpushed; T091 bumps the pin once it is on main).
  • T030: WIP mf() used jq -er, which exits 1 for false as well as null, so every container with default features: died under set -e — fixed. Multi-line run: | hooks shifted into name (one jq call for three fields) — fixed. PATH= cannot live in the env file (dotenv reads it literally) — gitfwd is on PATH via an image symlink; GITFWD_SPACE is exported too. agents.claude.mcp is Slug[] with no URL, so the entrypoint cannot register MCP servers — the legacy claude 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-357 are CE-specific (T060’s write-dev-env.sh), not entrypoint material; the GFP/1 block lives in the branch worktree at deploy/scripts/dev-entrypoint.sh:88-158 + :485-497, not in develop’s file. tailnet-prune.sh is host-side (T021/T031), nothing of it belongs in the image. Base image is node:22-bookworm-slim (apt sources + postgresql-15 are bookworm’s). Build context is the REPO ROOT (docker build -f images/base/Dockerfile .) — needs a root .dockerignore (T031). T031 contract: the entrypoint expects FACTORY_MANIFEST (resolved JSON, actorLabel + tailscale.hostname rendered; unrendered {…} is FATAL), FACTORY_ENV_FILE, FACTORY_USER (GITHUB_USERNAME alias) — see images/README.md. Docker daemon unavailable on this box: image build/boot acceptance deferred.

Errata found during implementation (Wave 3, 2026-09-16)

  • T031: hostDir is /opt/factory for 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.env is enforced engine-side (env -i + only the allow-list in docker exec) and in the entrypoint (sudo --preserve-env=<names>); factory-sync runs hooks in a plain subshell. access.github.org needs host secret GITHUB_ORG_READ_TOKEN for 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 '). A public: false default port → no DNS record, public probe skipped with a warning. Manifest resources.*.adopted DELETED from the schema (strict object refuses it as unknown). Needs a real host: Recreating over a legacy project with unchanged volume CreatedAt; root-0600 bind mounts readable under sysbox-runc user-ns; sudo --preserve-env as root in the image; single-use authkey re-join; Upstash management API shape; Traefik 404-vs-forward.

  • T024/T041: gateway.host: api.clanker.net declares a CNAME while the PRODUCT’s api.clanker.net is live in the same zone → host up collides at host-gateway-dns until T113. T023 step 4 must set gateway.host to a temporary name under clanker.net (and dogfood.yml’s host dispatch input smokes the tunnel hostname) until T112’s grace period ends. plan host mints the host keypair (the one side effect a plan has). Redis: spec databaseName: clanker-dev-host (legacy Upstash name), manifest-facing name: clanker-redis. Commit fcf6902 absorbed 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/releaseAppPrivateKey in ESC); operator creates App + v* ruleset TOGETHER. Trigger is release-shaped (v[0-9]+.[0-9]+.[0-9]+*), not bare v*, because the App-token push of the floats would re-fire it. factory-gateway also gets :1/:1.x; docker-compose.gateway.yml defaults FACTORY_GATEWAY_TAG=1. Docker unavailable here: neither the ci smoke nor a release build has run.

  • T121 follow-up: CE’s renderFooter ignores NavLink.attrs (CN footer app link lost data-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.md had claimed CE keeps check/env/chrome under site/; 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.ts NAV/FOOTER data, prepare-static-site.mjs routes, check-static-site.mjs required 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 NO no-secret-outputs sibling (that is CN’s), and assert-state-clean.mjs is invoked by nothing. Pre-existing, out of scope: isRealDeployment("replit-production") is false, so Replit production trusts x-forwarded-host.

  • T051: release.yml + docs/releasing.md + scripts/release-tags.mjs (+ tests/release-{workflow,tags}.test.ts, the gateway-image job in ci.yml). actions/attest-build-provenance cannot run while CN is private: the attestation API is a paid feature on a private repo and CE’s sandbox-build-image.yml deleted the step after “Feature not available for the clankerlabs organization” on every run — so the two signed steps are gated (verify output attest: public repo, or repo variable FACTORY_SIGNED_ATTESTATIONS=true), BuildKit provenance mode=max + sbom: true referrers 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 direct git push origin v1 is refused” needs a GitHub App as the bypass actor: releaseAppId/releaseAppPrivateKey seeded in ESC (rule 9 holds — read with PULUMI_ACCESS_TOKEN), minted per run with actions/create-github-app-token scoped to contents: write; until seeded the floats move with GITHUB_TOKEN under a ::warning::, which works only while no ruleset exists (operator does ruleset + App together, docs/releasing.md §Operator). The trigger is tags: ["v[0-9]+.[0-9]+.[0-9]+*"], not bare v*: an App-token push STARTS workflows (GITHUB_TOKEN’s do not), so a v* trigger would re-fire on the floats release.yml itself pushes and fail twice per release. A tags-only push trigger is exempt from branches-ignore in no-gitfwd-collision.test.ts (GitHub never fires it for a branch; adding the ignore beside tags: would make it fire on every branch push) — CLAUDE.md rule 3 reworded. factory-gateway gets the :1/:1.x floats too (the task named only :<tag>): host/docker-compose.gateway.yml defaults FACTORY_GATEWAY_TAG to 1, 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.1 behind v1.2.0 moves v1.0 only. The GHCR push credential is the ambient GITHUB_TOKEN (packages: write on the images job) — 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.md already carried the image.pull.tokenFrom limitation line — unchanged. Not verified here: no Docker daemon on this box, so neither the gateway-image smoke nor a release build has executed; every run: block is bash -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.ts keygen) 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 + root factory.yml (+ Procfile.dev) + dogfood.yml caller + docs/quickstart.md + tests/{sha-pinned-uses,factory-deploy-workflow}.test.ts + engine test/{dogfood-manifest,quickstart-doc,outputs-gate}.test.ts. A reusable workflow cannot uses: its own composite at its own SHA: uses: takes no expression, a ./ path resolves against the CALLER’s workspace, and the github context in a called workflow is the caller’s (workflow_sha is the caller’s file; there is no job_workflow_sha in the context, only in the OIDC claim, which needs the id-token: write this workflow deliberately does not hold). So the engine and composite come from a SECOND checkout — clankerlabs/CLANKERNET at factory_ref into .factory/, uses: ./.factory/.github/actions/setup-factory — and factory_ref is a caller input that defaults to the v1 float: an adopter pins it to the same 40-char SHA as the uses: line (docs/quickstart.md; Dependabot bumps only the uses: — the second pin is by hand, pinned by quickstart-doc.test.ts to match in the doc). While CN is private the engine checkout needs a token that reads CNactions/checkout with the caller’s GITHUB_TOKEN cannot clone a sibling private repo (the org “Actions access” setting covers uses: resolution only) — so HOSTS_REPO_TOKEN doubles 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’s uses: 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 gains factory-deploy.yml (it matches /deploy/) and pins it workflow_call-only. The composite’s npm ci is npm ci --omit=dev --workspace packages/engine (271 packages, 3 s, factory --help + validate + plan proven on a clean archive) — install: full for ci.yml/release.yml’s verify, install: none where only the CLI is needed (deploy.yml publish, release.yml release, which gained a checkout FIRST so git clean cannot eat the downloaded artifacts); every previous curl … get.pulumi.com is gone, the pin has ONE site. factory --help used 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’s host input) now accepts a host ID or a path like every other verb — isHostRefForm moved to hostref.ts and container/input.ts shares it; validate --json gained stack: {provider, name, backendUrl} (NON-secret, computed without the backend’s env(), which throws on a missing credential) so the workflow’s pulumi login + gate know the stack without a second parser; StateBackend gained backendUrl. The gate is pulumi stack output --json --stack … --cwd $FACTORY_ROOT/pulumi (the CLI wants a project) piped into packages/engine/scripts/no-secret-outputs.ts, which shares src/outputs-gate.ts with the engine’s own post-up assertOutputKeys (one allow-list, two readers; it also refuses a [secret] value, which the engine’s check did not). resource (for resources-destroy --name) and allow_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, no health — nobody starts it on provision), Procfile.dev is one window. Not done here: nothing has RUN on GitHub Actions — no PULUMI_ACCESS_TOKEN, no ESC environment, no host — so dogfood.yml action=provision (the T050 acceptance) is unexecuted; every run: block is bash -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-interactive against an s3 backend, and whether pulumi stack output on a passphrase stack needs PULUMI_CONFIG_PASSPHRASE (it is passed anyway). The deploy.yml gateway rollout job is NOT appended (the marker stays): host-converge on every trunk push has no host until T023.

Errata found during implementation (Wave 7 — checkout token, 2026-09-17)

  • T050/T103: HOSTS_REPO_TOKEN deleted 2026-09-17 — the checkout token is Pulumi config on the caller’s stack (checkout_token_from: <dir>:<stack>:<key>, read with pulumi config get at the composite’s Pulumi pin, masked; the engine reads it as FACTORY_GITHUB_TOKEN), USER.
  • T021/T023 (USER 2026-09-17: no new credentials, reuse clanker-infra): resources.upstash[] gained an existing form{ 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 CE shared/cache.ts’s existingRedisUrl short-circuit. An existing entry declares NO Pulumi resource (programLogicalNames omits it; host down does not count it), needs NO management credential (providerEnv sets UPSTASH_* only for a create entry), the container engine takes the REST pair straight from the two references (no management-API list), and resources destroy REFUSES it (“not ours to destroy”). hosts/dev-host.yml is now clanker-redis: existing { secret:REDIS_URL, secret:REDIS_TOKEN }databaseName: clanker-dev-host is GONE (so is the D23 “Redis created under the legacy name” clause: the legacy dev-host never owned a Redis; CE’s createCache short-circuits on app:redisUrl and dev-host-container.yml read staging’s rows), and the dev-host plan holds EIGHT resources, not nine (host-program.test.ts). upstashEmail/upstashApiKey are no longer referenced anywhere; UPSTASH_EMAIL/UPSTASH_API_KEY left seed-dev-host.yml and the seed script, which now seeds REDIS_URL/REDIS_TOKEN from staging. Pre-existing fact, unchanged, now written down: staging, production AND every dev container share that ONE Upstash database (seeding.md table). factory validate WARNS 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 developmain 2026-09-17, USER; Replit tracks main. Every workflow, guard test, hook, factory.yml, skill and doc now names main; both Pages projects were re-pointed (production_branch: main); develop and feat/factory-v2 are deleted on origin. D22’s “no main” clause is inverted (no develop) — 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 live marketplace.<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 by Site — Refresh Marketplace Catalogue as a PR) and the Replit build’s first command scripts/assert-site-released.sh refuses a runtime whose commit has no green release (GITHUB_TOKEN Replit Secret; CLANKER_SKIP_RELEASE_GATE=1 emergency-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.yml publish gained an attach step (production only, after wrangler pages deploy, reusing the cloudflare step’s outputs because a masked value cannot cross a job boundary): per pair (clankernet-publicclanker.net, clankernet-appapp.clanker.net) it deletes ONLY the two known orphans (CNAME -> clanker-public.pages.dev / clanker-app.pages.dev — CE’s clanker.engineer apply 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, ensures CNAME <name> -> <project>.pages.dev proxied + comment managed-by=clankernet/deploy (the apex is CNAME-flattened by Cloudflare automatically), POSTs the domain to the project, waits ≤3 min for active (warn-not-fail). The smoke runs its probe sets against https://clanker.net/https://app.clanker.net as 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.net is unchanged (host program, T023). Pinned by tests/deploy-workflow.test.ts.

Errata found during implementation (Wave 6 — D24 standardisation, 2026-09-16)

  • D24/T023: host id dev-host KEPT for now (hosts/dev-host.yml, examples/hosts/dev-host.yml, pulumi/Pulumi.dev-host.yaml, stack whitehatgg/clanker-infra/dev-host, Tailscale hostname dev-host), names clanker-dev-host / clanker-storage-dev-host / Upstash clanker-dev-host (manifest-facing clanker-redis) unchanged — USER 2026-09-16 revision: development + clanker-*-development is the TARGET, a one-line spec edit plus the CE rename in T081 once the legacy dev-host is gone; the clankerlabs-hel1 files are deleted. Supersedes D23’s “legacy names” clause only in that the names are now a deferred rename, not a permanent contract. pulumi/Pulumi.yaml is clanker-infra (PULUMI_PROJECT in layout.ts, the schema default for state.pulumiCloud.project, and the Automation API projectName — a mismatch with the workDir’s Pulumi.yaml is a Pulumi error, so the three are one constant). The stack config key is clanker-infra:host (the engine reads the bare host key, project-namespaced), not clankernet:host. pulumi config needs a project directory: the pulumi-config provider now passes --cwd <root>/pulumi on every argv (it did not, so the BYOC pulumi-config path could never have run outside pulumi/); secretsProvider() therefore takes deps.root. Same-name collision guard: StackHandle.export() + assertStackOwned in openHostStack — 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 outside programLogicalNames(spec); stack + provider resources ignored; empty passes. Encryption-key trap (not in the plan): Pulumi Cloud keys secure: values per STACK, so pulumi 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 legacy dev-host config has no committed file: scripts/seed-from-clanker-infra.sh recovers it with pulumi config refresh into a scratch project (fallback: the four --show-secrets stack outputs), and reads staging/production from CE’s committed files. deploy.yml/release.yml read with pulumi stack select … --cwd pulumi + pulumi config get NAME --cwd pulumi (vars.FACTORY_HOST, default dev-host; vars.FACTORY_ESC_ENVIRONMENT is gone). tests/no-committed-blobs.test.ts is now CE’s rule (ciphertext allowed under pulumi/; plaintext credential SHAPES refused under pulumi/, hosts/, examples/; every referenced secret:NAME is 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): .dockerignore allow-list, the gateway Dockerfile/entrypoint/auth-bridge (../../client/src/settings.js), ci-app.yml(jobclient, group ci-client-*), deploy.yml, release.yml, ci.yml, the husky/prettier/eslint ignores, check-lockfile-registry.mjs, four tests; the expo-router directory INSIDE it stays client/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.engineer all 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 unless Production — Release succeeded 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 by deploy.yml: orphans deleted, tagged CNAMEs, domains active; clanker.net + app.clanker.net 200). Trunk is main (renamed from develop; Replit project tracks it); the only other branch is the gitfwd/** lane. Upstash = the existing shared database (REDIS_URL/REDIS_TOKEN from staging), no management key. Seed lacks only the CN OAuth app pair (T042, owed).
  • api.clanker.net is an orphaned A to Replit’s ingress until host up puts the gateway there (temporary gateway.host is now unnecessary — the product no longer holds the name; flip hosts/dev-host.yml back to api.clanker.net before 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) — see docs/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_deletion reason, preclean_api_records, reset_pages_domains with --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 b877f13 on feat/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.md is a symlink to it, so the workflow CTX paths still resolve. Edit the file, not the symlink target path.
  • Branch consolidation: stale local feat/factory deleted. GitFWD fix/empty-document-title (2 expo fixes after the vendored pin) was fast-forwarded into chore/expo-moved-to-clankernet, remote branch deleted, and both commits ported into app/ as a1ad355, bf32aeb (app tests 59/59). feat/gitfwd-coordination NOT merged, per T082.
  • All three working branches are pushed: CN feat/factory-v2, CE feat/factory-adoption (== develop), GF chore/expo-moved-to-clankernet.