diff --git a/PLAN.md b/PLAN.md index 3b3047c..966f3d2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -553,8 +553,8 @@ The roadmap in ยง4 is what we're shipping. This section is *where we are right n | M5 | Privacy via Cloudflare Access | ๐Ÿงช implemented, not yet live-validated | Worker: `accessGuard` middleware verifies the `Cf-Access-Jwt-Assertion` JWT (RS256 via WebCrypto + JWKS cache, no `jose`) and gates `/` + `/r/*` + `/api/*`; enforces only when `ACCESS_AUD` var is set, so public mirrors stay open. `/webhooks/github` + `/health` left unauth. 8 unit tests. CLI: opt-in `gitflare access enable/disable` creates/deletes a self-hosted Access app + allow-list policy and redeploys with `ACCESS_AUD`/`ACCESS_TEAM_DOMAIN` vars. **Caveat:** this gates the dashboard/API only โ€” `git clone` hits Artifacts directly (`*.artifacts.cloudflare.net`), so it is NOT Access-gated; private-clone is a later (v0.4+) item per ยง6/ยง11. **TODO before โœ…:** verify the live Access apps/policies API shapes + `aud` field, and that `*.workers.dev` hosts are accepted, against a real account. | | M5.5 | v0.1 polish | ๐Ÿงช implemented | Syntax highlighting in the blob viewer (highlight.js core + ~20 curated grammars, server-side; 512 KB cap with `
` fallback; bundle 681โ†’901 KB). README image proxy: new `GET /r/:name/raw/*` serves blob bytes from the Artifacts mirror so images render for private repos and survive GitHub outages โ€” README rewriting now points there instead of `raw.githubusercontent.com`. Styled empty/error states (`ui/states.tsx`): home shows the `npx gitflare init` command, browse 404/500 render through the layout with a way back. |
 | M6 | v0.2 CD (MVP slice) | ๐Ÿงช implemented | **Self-deploy model (user chose Worker-Secret path).** On push, after sync, the webhook fires `DeployDO` (`waitUntil`), which clones the repo, parses `.gitflare/deploy.yml`, and uploads the pre-built `cloudflare/deploy { kind: worker }` entry via the Workers Scripts multipart API. History in `DeployDO`; Deployments UI; CLI `gitflare deploy enable/disable` stores `CF_DEPLOY_TOKEN` + `CD_ENABLED`. Superseded by M7. |
-| M7 | v0.2 CD โ€” complete (per ยง4) | ๐Ÿงช implemented, not yet live-validated | Everything in ยง4 v0.2: **(1)** real YAML-subset parser (`deploy/yaml.ts`, no heavyweight dep) โ†’ **worker bindings** (vars/KV/R2/D1/DO/services) in the Scripts metadata; **(2)** **Pages** deploys via Direct Upload (upload-token โ†’ check-missing โ†’ upload โ†’ create-deployment), with per-branch **previews** (`production_branch`); **(3)** **D1 migrations** applied in order via the D1 query API, opt-in (`apply: true`), idempotent (applied set tracked per-DB in the DO); **(4)** **live deploy-log streaming** over a hibernatable WebSocket (`/r/:name/deployments/stream`) with the Deployments page subscribing; **(5)** **manual / GitHub-down trigger** `gitflare deploy run` + `/control/deploy/run` (auth'd by a `CONTROL_SECRET`, outside Access); **(6)** `gitflare deploy list` + `gitflare deploy rollback [--to ]` (rollback redeploys a previous successful commit via a full clone; migrations are forward-only and skipped). 56 unit tests across yaml/workflow/cf-deploy/highlight/access. **TODO before โœ…:** one live run against a real Cloudflare account to confirm the Scripts multipart, Pages Direct Upload, and D1 query wire shapes. Known quirk (kept in M8): the v0.2 push path reads workflow/content at the clone's HEAD while recording the webhook sha โ€” same-branch seconds-apart race only; the v0.3 CI path reads strictly at the pushed sha by construction. |
-| M8 | v0.3 core CI โ€” sandbox jobs, needs-gated deploys | โœ… core live-validated (2026-07-17, `sinameraji/kimiflare`) | **Format** (`.gitflare/ci.yml`, parsed by `src/ci/workflow.ts` reusing the yaml.ts subset + the shared `cloudflare/deploy` step validator): `on`/`branches`, `jobs:` with `needs:` (parse-time unknown/cycle validation, Kahn topo order), `run:` steps (multi-line via new `\|`/`\|-` block-scalar support in yaml.ts), per-job `env:` + `timeout_minutes` (default 15, max 60); a job is either a run job or a deploy job (mixing = parse error). **Execution**: new `CiDO` (binding `CI`, migration v3) โ€” POST `/run` answers 202 and the pipeline runs as detached DO work; ONE sandbox per run (`@cloudflare/sandbox@0.12.3`, image pinned to match) cloned once from the Artifacts mirror via env-scoped `http.extraHeader` auth (token never in argv/.git/config/stderr; every log line scrubbed for the secret + its base64), then jobs execute in topo order with streamed line-buffered logs (hibernatable WS at `/r/:name/ci/stream`, cap 1000 lines/job). **Deploy jobs delegate to DeployDO** (`mode: "ci"`, strict at-sha ref-aware clone via new `cloneRepoAtRef`, feature branches included) so deploy history/rollback stay in one place; `DeployRecord` gains `workflow`/`job` for cross-format rollback. **Artifact handover**: worker entries built by run jobs are read out of the sandbox (5 MB cap) and shipped via `entryOverrides` โ€” deploys ship what CI built, not the stale committed file. **Safety**: alarm watchdog fails interrupted/over-budget runs + destroys orphaned sandboxes; cancel (CLI `gitflare ci cancel`, `/control/ci/cancel`, runs-page button); push-storm supersede; fail-closed โ€” committed ci.yml + CI disabled means deploy.yml does NOT run ungated; delegated deploys count as green ONLY on `status: "success"` (a "CD not enabled" skip fails the job loudly). **Status postback** via the legacy Commit Status API (context `gitflare/ci`; classic PAT can't use Checks), soft-fail. **CLI**: `gitflare ci enable/disable/run/list/cancel`; enable provisions `[[containers]]` (public `docker.io/cloudflare/sandbox` image โ€” no local Docker; `--instance-type`, default `standard-1`); migrations v3+v4 are emitted UNconditionally so cross-machine redeploys never miss applied tags; CONTROL_SECRET reuse is symmetric with `deploy enable`; disable drops only `CI_ENABLED` (idle containers cost $0). 51 new unit tests (97 total worker-side). Worker bundle 901 KB โ†’ 1557 KB (sandbox SDK +433 KB, capnweb +104 KB, containers +75 KB); ~400 KB gzipped, fine for Workers limits. **Deliberate deviations from ยง4 v0.3**: `runtime: worker` rejected with a clear error (Dynamic Workers can't run npm โ€” no Linux userland); per-job `image:` rejected (containers pin the image at Worker-deploy time); ArtifactFS mount not used (sandbox `git clone`; cold container start pulls the image from Docker Hub โ€” minutes, not the ยง4 \<500ms); jobs in one run share a sandbox/workspace (GHA users expect isolation โ€” M9); Pages artifact handover, R2 build cache, Browser Run, Actions importer, per-job status contexts, `gitflare sandbox` verb โ†’ M9. **Post-review hardening** (a 4-lens adversarial review of the diff, 18 findings, all verifier-confirmed): FIXED โ€” watchdog/pipeline verdict races (a `finalizedRunIds` fence stops a lagging coroutine resurrecting a run the watchdog failed, and the watchdog no longer erases a concurrent cancel); a fallback alarm armed in `begin()` so eviction before the deadline alarm can't leave a zombie "running" record; `ARTIFACTS.get` moved inside the try (+ a last-resort commit status) so a push can't vanish traceless; tag/non-branch pushes skipped at the webhook (were minting failed runs + red statuses); artifact handover gated on the whole `needs` closure of run jobs succeeding (was shipping a failed build's output); the dead "fall back to deploy.yml" path on missing SANDBOX replaced with an honest fail-closed error; delegated-deploy response mapping extracted to a tested pure fn (`ci/delegation.ts`, all four shapes incl. skipped-is-not-green); the remote-URL allowlist tightened to reject shell metacharacters; duplicate webhook deliveries de-duped by branch+sha; dashboard cancel gated behind Access (was open on public mirrors โ€” CLI `gitflare ci cancel` uses CONTROL_SECRET); CONTROL_SECRET now always (re)written on enable and config persisted the moment the Worker goes live (was strandable across machines / on a partial enable). RESIDUAL (documented, not yet fixed โ€” acceptable for a not-live-validated milestone): a run still queued in-memory when the DO is evicted (e.g. by a redeploy) is dropped with no record; webhook delivery order isn't guaranteed by GitHub, so the supersede optimization is best-effort (out-of-order delivery can run the older sha last); cancel is a no-op during a run's brief pre-clone window and cannot un-ship a deploy job that already returned success (the run message says so). **Live-validated end-to-end** (2026-07-17, `sinameraji/kimiflare` on a real Workers Paid account): `gitflare ci enable` provisions the `[[containers]]` app from wrangler TOML; a push โ†’ webhook โ†’ sync โ†’ CI run #1 succeeded in 12s โ€” the Sandbox booted (`node v22`), cloned the mirror at the pushed sha with a short-lived read token (URL logged WITHOUT credentials โ€” env-scoped auth + scrubbing confirmed), and ran all 4 `run:` steps with streamed line-buffered logs, resolved env vars, and captured exit codes. Findings fixed along the way (all on this branch): (1) container deploy needs BOTH **Account โ†’ Cloudchamber:Edit** and **Account โ†’ Containers:Edit** token permissions (Containers alone โ†’ `Forbidden`); (2) the container app name can't contain consecutive dashes, so we emit an explicit sanitized `name` (the `owner--repo` worker name has `--`); (3) a latent M1 sync bug โ€” `artifactsRepo.remote` on the current Artifacts beta is a lazy RPC proxy that stringifies to `[object JsRpcProperty]`; now threads the REPO_MAP remote string (would have broken any live v0.2 deploy too); (4) the push webhook awaited the full sync and blew past GitHub's 10s delivery timeout on a real repo (504 + redeliveries) โ€” now responds 202 immediately, sync + dispatch in `waitUntil`; (5) commit-status postback soft-fails observably (the run logs *why* โ€” here, an expired GITHUB_TOKEN). Config also cross-checked via `wrangler deploy --dry-run` (both CI-provisioned and default TOML resolve all four DO exports incl. unbound-Sandbox migration v4). **Not yet exercised live** (deferred soak): the deploy-job โ†’ DeployDO delegation, the alarm watchdog firing, cancel mid-run, hour-scale runs / `sleepAfter` container-activity behavior, and commit statuses actually rendering (needs a fresh GITHUB_TOKEN). |
+| M7 | v0.2 CD โ€” complete (per ยง4) | ๐Ÿงช worker-deploy path live-validated; Pages + D1 pending | Everything in ยง4 v0.2: **(1)** real YAML-subset parser (`deploy/yaml.ts`, no heavyweight dep) โ†’ **worker bindings** (vars/KV/R2/D1/DO/services) in the Scripts metadata; **(2)** **Pages** deploys via Direct Upload (upload-token โ†’ check-missing โ†’ upload โ†’ create-deployment), with per-branch **previews** (`production_branch`); **(3)** **D1 migrations** applied in order via the D1 query API, opt-in (`apply: true`), idempotent (applied set tracked per-DB in the DO); **(4)** **live deploy-log streaming** over a hibernatable WebSocket (`/r/:name/deployments/stream`) with the Deployments page subscribing; **(5)** **manual / GitHub-down trigger** `gitflare deploy run` + `/control/deploy/run` (auth'd by a `CONTROL_SECRET`, outside Access); **(6)** `gitflare deploy list` + `gitflare deploy rollback [--to ]` (rollback redeploys a previous successful commit via a full clone; migrations are forward-only and skipped). 56 unit tests across yaml/workflow/cf-deploy/highlight/access. **Live status (2026-07-17):** the **worker** deploy path (`uploadWorkerScript`: Scripts multipart upload + bindings + the workers.dev-subdomain enable fix) is validated โ€” the v0.3 CI deploy job delegates through this exact code and shipped a reachable worker (see M8). The **sync** the deploy runs on top of is validated too (the `JsRpcProperty` remote fix). **Still pending a live run:** Pages Direct Upload and the D1 migration query path (both untouched by the CI validation). Known quirk (kept in M8): the v0.2 push path reads workflow/content at the clone's HEAD while recording the webhook sha โ€” same-branch seconds-apart race only; the v0.3 CI path reads strictly at the pushed sha by construction. |
+| M8 | v0.3 core CI โ€” sandbox jobs, needs-gated deploys | โœ… core live-validated (2026-07-17, `sinameraji/kimiflare`) | **Format** (`.gitflare/ci.yml`, parsed by `src/ci/workflow.ts` reusing the yaml.ts subset + the shared `cloudflare/deploy` step validator): `on`/`branches`, `jobs:` with `needs:` (parse-time unknown/cycle validation, Kahn topo order), `run:` steps (multi-line via new `\|`/`\|-` block-scalar support in yaml.ts), per-job `env:` + `timeout_minutes` (default 15, max 60); a job is either a run job or a deploy job (mixing = parse error). **Execution**: new `CiDO` (binding `CI`, migration v3) โ€” POST `/run` answers 202 and the pipeline runs as detached DO work; ONE sandbox per run (`@cloudflare/sandbox@0.12.3`, image pinned to match) cloned once from the Artifacts mirror via env-scoped `http.extraHeader` auth (token never in argv/.git/config/stderr; every log line scrubbed for the secret + its base64), then jobs execute in topo order with streamed line-buffered logs (hibernatable WS at `/r/:name/ci/stream`, cap 1000 lines/job). **Deploy jobs delegate to DeployDO** (`mode: "ci"`, strict at-sha ref-aware clone via new `cloneRepoAtRef`, feature branches included) so deploy history/rollback stay in one place; `DeployRecord` gains `workflow`/`job` for cross-format rollback. **Artifact handover**: worker entries built by run jobs are read out of the sandbox (5 MB cap) and shipped via `entryOverrides` โ€” deploys ship what CI built, not the stale committed file. **Safety**: alarm watchdog fails interrupted/over-budget runs + destroys orphaned sandboxes; cancel (CLI `gitflare ci cancel`, `/control/ci/cancel`, runs-page button); push-storm supersede; fail-closed โ€” committed ci.yml + CI disabled means deploy.yml does NOT run ungated; delegated deploys count as green ONLY on `status: "success"` (a "CD not enabled" skip fails the job loudly). **Status postback** via the legacy Commit Status API (context `gitflare/ci`; classic PAT can't use Checks), soft-fail. **CLI**: `gitflare ci enable/disable/run/list/cancel`; enable provisions `[[containers]]` (public `docker.io/cloudflare/sandbox` image โ€” no local Docker; `--instance-type`, default `standard-1`); migrations v3+v4 are emitted UNconditionally so cross-machine redeploys never miss applied tags; CONTROL_SECRET reuse is symmetric with `deploy enable`; disable drops only `CI_ENABLED` (idle containers cost $0). 51 new unit tests (97 total worker-side). Worker bundle 901 KB โ†’ 1557 KB (sandbox SDK +433 KB, capnweb +104 KB, containers +75 KB); ~400 KB gzipped, fine for Workers limits. **Deliberate deviations from ยง4 v0.3**: `runtime: worker` rejected with a clear error (Dynamic Workers can't run npm โ€” no Linux userland); per-job `image:` rejected (containers pin the image at Worker-deploy time); ArtifactFS mount not used (sandbox `git clone`; cold container start pulls the image from Docker Hub โ€” minutes, not the ยง4 \<500ms); jobs in one run share a sandbox/workspace (GHA users expect isolation โ€” M9); Pages artifact handover, R2 build cache, Browser Run, Actions importer, per-job status contexts, `gitflare sandbox` verb โ†’ M9. **Post-review hardening** (a 4-lens adversarial review of the diff, 18 findings, all verifier-confirmed): FIXED โ€” watchdog/pipeline verdict races (a `finalizedRunIds` fence stops a lagging coroutine resurrecting a run the watchdog failed, and the watchdog no longer erases a concurrent cancel); a fallback alarm armed in `begin()` so eviction before the deadline alarm can't leave a zombie "running" record; `ARTIFACTS.get` moved inside the try (+ a last-resort commit status) so a push can't vanish traceless; tag/non-branch pushes skipped at the webhook (were minting failed runs + red statuses); artifact handover gated on the whole `needs` closure of run jobs succeeding (was shipping a failed build's output); the dead "fall back to deploy.yml" path on missing SANDBOX replaced with an honest fail-closed error; delegated-deploy response mapping extracted to a tested pure fn (`ci/delegation.ts`, all four shapes incl. skipped-is-not-green); the remote-URL allowlist tightened to reject shell metacharacters; duplicate webhook deliveries de-duped by branch+sha; dashboard cancel gated behind Access (was open on public mirrors โ€” CLI `gitflare ci cancel` uses CONTROL_SECRET); CONTROL_SECRET now always (re)written on enable and config persisted the moment the Worker goes live (was strandable across machines / on a partial enable). RESIDUAL (documented, not yet fixed โ€” acceptable for a not-live-validated milestone): a run still queued in-memory when the DO is evicted (e.g. by a redeploy) is dropped with no record; webhook delivery order isn't guaranteed by GitHub, so the supersede optimization is best-effort (out-of-order delivery can run the older sha last); cancel is a no-op during a run's brief pre-clone window and cannot un-ship a deploy job that already returned success (the run message says so). **Live-validated end-to-end** (2026-07-17, `sinameraji/kimiflare` on a real Workers Paid account): `gitflare ci enable` provisions the `[[containers]]` app from wrangler TOML; a push โ†’ webhook โ†’ sync โ†’ CI run #1 succeeded in 12s โ€” the Sandbox booted (`node v22`), cloned the mirror at the pushed sha with a short-lived read token (URL logged WITHOUT credentials โ€” env-scoped auth + scrubbing confirmed), and ran all 4 `run:` steps with streamed line-buffered logs, resolved env vars, and captured exit codes. Findings fixed along the way (all on this branch): (1) container deploy needs BOTH **Account โ†’ Cloudchamber:Edit** and **Account โ†’ Containers:Edit** token permissions (Containers alone โ†’ `Forbidden`); (2) the container app name can't contain consecutive dashes, so we emit an explicit sanitized `name` (the `owner--repo` worker name has `--`); (3) a latent M1 sync bug โ€” `artifactsRepo.remote` on the current Artifacts beta is a lazy RPC proxy that stringifies to `[object JsRpcProperty]`; now threads the REPO_MAP remote string (would have broken any live v0.2 deploy too); (4) the push webhook awaited the full sync and blew past GitHub's 10s delivery timeout on a real repo (504 + redeliveries) โ€” now responds 202 immediately, sync + dispatch in `waitUntil`; (5) commit-status postback soft-fails observably (the run logs *why* โ€” here, an expired GITHUB_TOKEN). Config also cross-checked via `wrangler deploy --dry-run` (both CI-provisioned and default TOML resolve all four DO exports incl. unbound-Sandbox migration v4). **Deploy-job delegation also validated** (2026-07-17): a two-job `ci.yml` (a `run` job builds `dist/worker.js` in the sandbox โ€” NOT committed โ€” then a `needs`-gated deploy job) ran green end-to-end; the log shows `using CI-built dist/worker.js (103 bytes)` โ†’ the sandbox artifact flowed through `entryOverrides` to `DeployDO` and shipped, and the deployed worker served the CI-built body at **HTTP 200**. This surfaced + fixed a **latent v0.2 bug**: `DeployDO`'s raw Scripts-API upload never enabled the `workers.dev` subdomain (wrangler does; the API doesn't), so deployed workers 404'd โ€” `uploadWorkerScript` now enables it and returns the reachable URL. Commit statuses also confirmed rendering on GitHub once a fresh `GITHUB_TOKEN` was set. **Not yet exercised live** (deferred soak): the alarm watchdog firing, cancel mid-run, and hour-scale runs / `sleepAfter` container-activity behavior. |
 
 ### What's in the repo right now (as of M0)
 
diff --git a/README.md b/README.md
index 08f9d7b..034d21b 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,8 @@ GitFlare ships in versions. Each one stands alone โ€” if the next one never gets
 | Version | Status | What it does |
 |---|---|---|
 | **v0.1** | โœ… **shipping โ€” you are here** | **Read replica.** One command mirrors a GitHub repo into your Cloudflare account: Artifacts for git storage, a Worker that takes GitHub webhooks + serves a dashboard, file browsing with syntax highlighting, README rendering (images proxied through your Worker), sync status. Optional Cloudflare Access gates the dashboard for private repos. If GitHub is down, reads + clones still work. |
-| v0.2 | ๐Ÿงช code-complete (pending a live run) | **CD that doesn't depend on GitHub.** Push โ†’ your Worker deploys to your own account: Workers + Pages (with preview deploys), bindings (vars/KV/R2/D1/DO/services), opt-in D1 migrations, live deploy logs over WebSocket, plus `deploy run` (the GitHub-down escape hatch), `deploy list`, and `deploy rollback`. Deploys **pre-built** artifacts via `.gitflare/deploy.yml`; arbitrary build steps arrive with v0.3 CI. |
-| v0.3 | ๐Ÿšง in progress (core CI live-validated) | **Generic CI.** `.gitflare/ci.yml` with jobs / `needs:` / `run:` steps, executed on Cloudflare Sandboxes (full Linux containers on your account) โ€” validated end-to-end: push โ†’ sandbox boots โ†’ clones โ†’ runs your steps with live logs. Deploy jobs gate on test jobs, and a Worker deploy ships **what CI just built** (not the stale committed file). Cancel, run history, GitHub commit statuses. Still to come in v0.3: R2 build cache, Browser Run for E2E, a GitHub Actions importer, Pages build artifacts. |
+| v0.2 | ๐Ÿงช Worker deploys live-validated; Pages + D1 pending | **CD that doesn't depend on GitHub.** Push โ†’ your Worker deploys to your own account: Workers + Pages (with preview deploys), bindings (vars/KV/R2/D1/DO/services), opt-in D1 migrations, live deploy logs over WebSocket, plus `deploy run` (the GitHub-down escape hatch), `deploy list`, and `deploy rollback`. Deploys **pre-built** artifacts via `.gitflare/deploy.yml`; arbitrary build steps arrive with v0.3 CI. |
+| v0.3 | ๐Ÿšง in progress (core CI live-validated) | **Generic CI.** `.gitflare/ci.yml` with jobs / `needs:` / `run:` steps, executed on Cloudflare Sandboxes (full Linux containers on your account) โ€” validated end-to-end: push โ†’ sandbox boots โ†’ clones โ†’ runs your steps with live logs, and a `needs`-gated deploy job ships **what CI just built** (not the stale committed file) to a reachable Worker. Cancel, run history, GitHub commit statuses. Still to come in v0.3: R2 build cache, Browser Run for E2E, a GitHub Actions importer, Pages build artifacts. |
 | v0.4 | ๐Ÿ“‹ planned | **Multi-user teams.** PRs, reviews, comments โ€” native to GitFlare, bidirectionally mirrored to GitHub. Stacked diffs. "Open PR in sandbox" one-click ephemeral env. |
 | v0.5 | ๐Ÿ“‹ planned | **Cross-tenant collaboration via Cloudflare Mesh.** Alice and Bob on separate Cloudflare accounts; private repos served Mesh-only with per-identity policies instead of SSH keys. |
 | v0.6 | ๐Ÿ“‹ planned | **Public repos + discovery.** A real code browser for the public web, search, forks across accounts. |
@@ -110,7 +110,7 @@ GitFlare never sees your code, your token, or your traffic. It's an MIT-licensed
 
 Pre-alpha, built in the open, and there's a lot of obvious next work. Cloudflare Access (M5), the full v0.2 CD feature set, and the v0.3 core CI (M8: sandbox jobs, needs-gated deploys, artifact handover) have landed โ€” see [PLAN.md ยง12](./PLAN.md#12-milestones-and-development-log) for current status. PRs and issues are welcome โ€” particularly on:
 
-- **Live-validating M5 + v0.2 + M8** against a real Cloudflare account. The Access apps/policies API, the Workers Scripts upload, Pages Direct Upload, the D1 query path, and now the Containers provisioning + Sandbox exec paths are coded to spec but need an end-to-end run.
+- **Live-validating the remaining CD paths.** The v0.3 CI stack (Containers provisioning, Sandbox exec, the Workers Scripts upload + workers.dev enablement) and the needs-gated deploy job are now validated end-to-end against a real Workers Paid account. Still needing a live run: **Pages Direct Upload**, the **D1 migration** query path, and **Cloudflare Access** (M5) apps/policies.
 - **v0.3 remainder.** R2 build cache keyed on lockfile hash, Browser Run for E2E, the GitHub Actions importer, and Pages build-artifact handover โ€” see [PLAN.md ยง12 M8 notes](./PLAN.md#12-milestones-and-development-log).
 - **Private `git clone`.** Access gates the dashboard, but clone still hits Artifacts directly. Closing that needs an Access service token / Mesh path (v0.4+).
 - **Custom domains** in front of the Worker, and **better empty states / error messages** anywhere in the CLI or dashboard.