|
| 1 | +--- |
| 2 | +name: internal-dev-workbench |
| 3 | +description: Spin up a portless + tmux dev session for the Workflow SDK that gives each git worktree isolated `<branch>.<name>.localhost` URLs for the Next.js workbench and the observability UI, plus a Claude statusline that surfaces those URLs. Use only when the user asks for a "portless dev session", a "tmux dev layout for workflow", "worktree-isolated dev URLs", or wants to wire workflow dev URLs into the Claude statusline. Do not activate for the generic "start the dev server" / "run pnpm dev" task. |
| 4 | +metadata: |
| 5 | + author: Pranay Prakash |
| 6 | + version: '0.1' |
| 7 | +--- |
| 8 | + |
| 9 | +# internal-dev-workbench |
| 10 | + |
| 11 | +Bootstraps an opinionated 3-pane tmux session for end-to-end Workflow SDK development. Each pane is launched through [portless](https://github.com/aleclarson/portless) so URLs are stable and worktree-scoped (e.g. `https://<branch>.turbopack.localhost`), letting multiple worktrees run concurrently without port conflicts. A companion statusline script surfaces the active URLs in Claude Code's prompt. |
| 12 | + |
| 13 | +This is **opt-in contributor tooling**. The repo's standard dev path (`pnpm dev` from a workbench, no portless) is unaffected. |
| 14 | + |
| 15 | +## Prerequisites |
| 16 | + |
| 17 | +- `tmux` installed |
| 18 | +- `portless` installed globally (`npm i -g portless` or via Homebrew). Verify with `portless --version`. |
| 19 | +- Repo bootstrapped: `pnpm install && pnpm build`. The first run on a fresh worktree must complete both before any dev server can start (the workbench apps depend on built workspace packages — without `pnpm build` you get `MODULE_NOT_FOUND` for `workflow`). |
| 20 | +- `WORKFLOW_PUBLIC_MANIFEST=1` is required on the dev server when running e2e tests against it (otherwise `/.well-known/workflow/v1/manifest.json` is gated). |
| 21 | + |
| 22 | +## Layout |
| 23 | + |
| 24 | +`main-vertical` — the dev server takes the left column; the right column stacks the observability UI on top of a scratchpad shell: |
| 25 | + |
| 26 | +``` |
| 27 | ++----------------------+--------------------------+ |
| 28 | +| | PANE_OBS: workflow web | |
| 29 | +| | (observability UI | |
| 30 | +| PANE_DEV: turbopack | scoped to the | |
| 31 | +| (Next.js dev) | workbench app) | |
| 32 | +| +--------------------------+ |
| 33 | +| | PANE_SH: zsh scratchpad | |
| 34 | +| | (repo root — for build, | |
| 35 | +| | tests, e2e, git, etc.) | |
| 36 | ++----------------------+--------------------------+ |
| 37 | +``` |
| 38 | + |
| 39 | +## Setup |
| 40 | + |
| 41 | +The session name **must** match the worktree's portless prefix — the basename of the current branch — so the statusline (and any other tooling that derives the prefix from the branch) can locate it. Always run `tmux ls` first to confirm there's no pre-existing session with that name; never kill an existing one. |
| 42 | + |
| 43 | +Pane indices in tmux depend on `pane-base-index` (0 by default, 1 with the common dotfile override). To stay correct under either, capture each pane's ID at split time with `-P -F '#{pane_id}'` and use those IDs as targets: |
| 44 | + |
| 45 | +```bash |
| 46 | +REPO=/path/to/workflow--<worktree-suffix> |
| 47 | +# Session name = basename of the branch (matches portless's subdomain prefix |
| 48 | +# and the statusline's `tmux attach -t <prefix>` indicator). For branch |
| 49 | +# `pgp/foo-bar` this resolves to `foo-bar`. |
| 50 | +SESSION=$(git -C "$REPO" rev-parse --abbrev-ref HEAD) |
| 51 | +SESSION="${SESSION##*/}" |
| 52 | + |
| 53 | +# Create the session and capture the initial pane ID |
| 54 | +PANE_DEV=$(tmux new-session -d -s "$SESSION" -c "$REPO" -P -F '#{pane_id}') |
| 55 | +PANE_OBS=$(tmux split-window -h -t "$PANE_DEV" -c "$REPO" -P -F '#{pane_id}') |
| 56 | +PANE_SH=$(tmux split-window -v -t "$PANE_OBS" -c "$REPO" -P -F '#{pane_id}') |
| 57 | +tmux select-layout -t "$SESSION" main-vertical |
| 58 | + |
| 59 | +# Pane DEV (left): Next.js turbopack workbench, with manifest exposed for e2e |
| 60 | +tmux send-keys -t "$PANE_DEV" \ |
| 61 | + 'cd workbench/nextjs-turbopack && WORKFLOW_PUBLIC_MANIFEST=1 portless run --name turbopack pnpm dev' C-m |
| 62 | + |
| 63 | +# Pane OBS (top-right): observability UI scoped to the workbench app |
| 64 | +tmux send-keys -t "$PANE_OBS" \ |
| 65 | + 'cd workbench/nextjs-turbopack && portless run --name workflow-obs sh -c "pnpm workflow web --webPort \$PORT --noBrowser"' C-m |
| 66 | + |
| 67 | +# Pane SH (bottom-right): scratchpad at repo root |
| 68 | +tmux send-keys -t "$PANE_SH" 'echo "scratchpad: $(pwd)"' C-m |
| 69 | + |
| 70 | +tmux attach -t "$SESSION" |
| 71 | +``` |
| 72 | + |
| 73 | +Once both servers are ready, `portless list` shows the routes. With `portless run`, each linked worktree gets a unique branch-prefixed subdomain (e.g. `stepflow-test.turbopack.localhost`), so multiple worktrees coexist without changing config. |
| 74 | + |
| 75 | +## Why each piece |
| 76 | + |
| 77 | +- **`portless run --name <name>`** (instead of `portless <name> <cmd>`): `run` auto-detects git worktrees and prepends the sanitized branch name as a subdomain. The `--name` flag overrides the inferred base name while preserving the worktree prefix. |
| 78 | +- **`pnpm workflow web --webPort $PORT --noBrowser`** (instead of `pnpm dev` in `packages/web`): the bundled CLI starts the observability UI configured against the **current workbench app**, hydrating it with that project's local World data. Running `packages/web`'s own `dev` script gives you the UI but pointed at nothing. |
| 79 | +- **`sh -c '... --webPort $PORT'`**: portless's auto `--port` injection only triggers for known frameworks it can detect on the command line. When the command is a CLI wrapper (`pnpm workflow web`), wrap in `sh -c` and read `$PORT` (which portless always sets) explicitly. |
| 80 | +- **`WORKFLOW_PUBLIC_MANIFEST=1`** on the dev pane: required for e2e tests to fetch the workflow registry from the dev server. |
| 81 | +- **`-P -F '#{pane_id}'`**: makes the snippet correct regardless of the user's `pane-base-index` setting (defaults vary across configs). |
| 82 | + |
| 83 | +## Claude statusline integration |
| 84 | + |
| 85 | +The skill ships a statusline helper at `skills/internal-dev-workbench/statusline.sh` that derives the worktree prefix from the current branch and emits a compact line: |
| 86 | + |
| 87 | +``` |
| 88 | + dev · obs · tmux attach -t <worktree-prefix> |
| 89 | +``` |
| 90 | + |
| 91 | +The dev / obs labels (Nerd Font rocket / graph glyphs) are OSC 8 hyperlinks — clickable in iTerm2, Kitty, WezTerm, Terminal.app, Ghostty — styled bold + underlined + bright cyan so they read unambiguously as links. The tmux fragment (Nerd Font copy glyph) is bold bright green, signaling "copy this" rather than "click this". It's shown only when a session named exactly the worktree prefix exists, and it's printed as a full ready-to-paste `tmux attach -t <prefix>` invocation. The font must include Nerd Font glyphs for the icons to render correctly; without them you'll see substitution boxes but the layout still works. Each piece is independent — if portless has no `<prefix>.turbopack.localhost` route, the dev fragment is omitted, and so on. With nothing to show, the script prints nothing and the statusline stays silent. |
| 92 | + |
| 93 | +Wire it into `~/.claude/settings.json` so it works across all sessions and worktrees. **Point the path at your primary checkout, not at a worktree** — worktrees get deleted, so any path like `~/github/vercel/workflow--<branch>/...` will break the day you remove that worktree: |
| 94 | + |
| 95 | +```json |
| 96 | +{ |
| 97 | + "statusLine": { |
| 98 | + "type": "command", |
| 99 | + "command": "$HOME/github/vercel/workflow/skills/internal-dev-workbench/statusline.sh" |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +Adjust the prefix if your main checkout lives elsewhere. The script itself is worktree-aware: it reads Claude's `workspace.current_dir` from stdin to derive the current branch, so the *same script invocation* from `~/github/vercel/workflow/...` correctly surfaces routes for whichever worktree the Claude session is running in. |
| 105 | + |
| 106 | +Output rules: |
| 107 | +- Nothing to show (no matching portless route, no matching tmux session) → empty output. |
| 108 | +- Each piece appears independently — start a server but no tmux session and you'll see just the dev/obs fragments; the reverse shows just the tmux fragment. |
| 109 | +- No git context but routes exist → falls back to the first matching `turbopack`/`workflow-obs` route, no tmux indicator. |
| 110 | + |
| 111 | +If you already use a statusline and want to append the internal-dev-workbench info, run the helper and concatenate in your existing wrapper script instead of replacing `command` outright. |
| 112 | + |
| 113 | +## Restarting after editing workflow files |
| 114 | + |
| 115 | +The workflow manifest is built at dev-server startup. New workflows or steps added to `workbench/example/workflows/*.ts` (and their symlinks in other workbenches) **do not appear at runtime** — even with HMR — until the dev server restarts. |
| 116 | + |
| 117 | +```bash |
| 118 | +tmux send-keys -t "$PANE_DEV" C-c |
| 119 | +# Wait for the prompt to return |
| 120 | +tmux send-keys -t "$PANE_DEV" \ |
| 121 | + 'cd workbench/nextjs-turbopack && WORKFLOW_PUBLIC_MANIFEST=1 portless run --name turbopack pnpm dev' C-m |
| 122 | +``` |
| 123 | + |
| 124 | +Verify the new workflow is registered (use the portless-assigned local port from `portless list`, or the `.localhost` URL with the trusted CA): |
| 125 | + |
| 126 | +```bash |
| 127 | +/usr/bin/curl -s "$(portless get turbopack)/.well-known/workflow/v1/manifest.json" \ |
| 128 | + | grep -o '<your-new-workflow>' |
| 129 | +``` |
| 130 | + |
| 131 | +`NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem` is needed for Node clients hitting the HTTPS URL outside of portless-managed children. Browsers are fine after `portless trust`. |
| 132 | + |
| 133 | +## Running e2e tests against this session |
| 134 | + |
| 135 | +From the scratchpad pane. Use the portless-assigned local port to bypass TLS for the test runner: |
| 136 | + |
| 137 | +```bash |
| 138 | +PORT=$(portless list | awk '/turbopack/ {n=split($3,a,":"); print a[n]; exit}') |
| 139 | +DEPLOYMENT_URL="http://localhost:$PORT" APP_NAME="nextjs-turbopack" \ |
| 140 | + pnpm vitest run packages/core/e2e/e2e.test.ts -t "<test name>" |
| 141 | +``` |
| 142 | + |
| 143 | +Or use the portless URL with the CA trust: |
| 144 | + |
| 145 | +```bash |
| 146 | +NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem \ |
| 147 | + DEPLOYMENT_URL="$(portless get turbopack)" APP_NAME="nextjs-turbopack" \ |
| 148 | + pnpm vitest run packages/core/e2e/e2e.test.ts -t "<test name>" |
| 149 | +``` |
| 150 | + |
| 151 | +## Teardown |
| 152 | + |
| 153 | +```bash |
| 154 | +tmux kill-session -t "$SESSION" |
| 155 | +``` |
| 156 | + |
| 157 | +Portless removes routes when each child process exits (Ctrl+C the panes first if you want a clean `portless list`). The proxy itself keeps running for other sessions; stop it explicitly with `portless proxy stop` if needed. |
| 158 | + |
| 159 | +## Troubleshooting |
| 160 | + |
| 161 | +- **`MODULE_NOT_FOUND: 'workflow'`** in the dev pane — workspace packages haven't been built. Run `pnpm build` from the repo root, then restart the pane. |
| 162 | +- **Observability UI shows no runs** — verify the obs pane was started from inside `workbench/nextjs-turbopack` (or whichever workbench you want to inspect). The CLI reads the local World from the **current working directory**. |
| 163 | +- **react-router on `:5173` instead of the portless port** — happens when the obs pane uses `pnpm dev` from `packages/web`. Switch to the `pnpm workflow web --webPort $PORT` form above. |
| 164 | +- **Source-map warning on startup** (`failed to read input source map ... packages/serde/dist/index.js.map`) — benign; doesn't block dev. |
| 165 | +- **Stale workflow registration** after editing `99_e2e.ts` — restart the dev pane; HMR doesn't rebuild the manifest. |
| 166 | +- **Statusline shows nothing** — confirm `portless list` has at least one matching route, the path in `settings.json` is absolute, and the script is executable (`chmod +x`). |
0 commit comments