Skip to content

Commit ab639a1

Browse files
pranaygpclaude
andauthored
Add dev-tmux skill for portless+tmux local Workflow SDK dev (vercel#1916)
* [e2e] Add step-vs-sleep race tests + dev-tmux skill Adds two race workflows (sleepWinsRaceWorkflow, stepWinsRaceWorkflow) that exercise Promise.race between a step function and a sleep call. The current `sleepWinsRaceWorkflow` test fails — surfacing how the replay engine resolves a previously-completed step instantly while sleep still has to elapse. Also adds a `dev-tmux` skill that documents the 3-pane tmux + portless setup for testing workflows interactively in a worktree alongside the observability UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [workbench/nextjs-turbopack] Allow *.turbopack.localhost in dev Adds allowedDevOrigins entries so portless-style worktree-prefixed .localhost URLs (e.g. https://<branch>.turbopack.localhost) can hit HMR and dev-only endpoints without Next's cross-origin protection flooding the logs with warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop duplicate race workflows after merging main PR vercel#1924 added the same sleep/step race workflows directly to main while this branch was open. The textual concat from `git merge` left both copies in 99_e2e.ts; this drops the duplicate set so the file matches origin/main verbatim and the e2e tests pick up the upstream definitions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Narrow activation, robust pane IDs, statusline helper - Tighten activation phrases so the skill only fires for the specific portless+tmux setup it documents, not the generic "start the dev server" task. Addresses vercel#1916 review. - Capture pane IDs at split time (-P -F '#{pane_id}') so the snippet works under both pane-base-index 0 and 1. Addresses Copilot review. - Add `statusline.sh` that filters `portless list` to the current worktree's routes and emits a one-line summary, plus instructions for wiring it into Claude Code's `statusLine.command`. - Bump version to 1.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Recommend primary-checkout path for statusline Worktrees get deleted, so wiring the statusline to a worktree path breaks the moment the worktree is removed. Update the skill and the script header to recommend pointing `statusLine.command` at the primary checkout (`$HOME/github/vercel/workflow/...`). The script itself is already worktree-aware via Claude's `workspace.current_dir` stdin JSON, so the same invocation surfaces routes for whichever worktree the session is in. Bump version to 1.2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] OSC 8 link statusline + worktree-named tmux session - Statusline overlay now renders `[dev] · [obs] · tmux:<prefix>`, with the bracketed labels emitted as OSC 8 hyperlinks (clickable in any modern terminal) styled cyan + underline so they stand out. Replaces the old long-URL form that was hard to scan and click. - Add a tmux-session indicator: shown when a session named exactly the worktree prefix exists (uses `tmux has-session -t =<prefix>` for exact matching). - Change the skill's tmux session naming convention from the fixed `workflow-dev` to `<worktree-prefix>` (basename of the branch — same string portless uses as the subdomain prefix). This lets the statusline locate the session deterministically and lets multiple worktrees run dev sessions concurrently without manual disambiguation. - Bump skill to v1.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Statusline: print full `tmux attach -t <name>` command Replaces the abbreviated `tmux:<prefix>` indicator with the full copy-paste-ready `tmux attach -t <prefix>` invocation. Saves a step when grabbing the session from another shell. Bump skill to v1.4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Brighter statusline + Nerd Font icons - Drop the dim styling that made the overlay hard to read; use bold bright cyan + underline for links and bold bright green for the tmux command. - Add Nerd Font glyphs: for dev, for obs, for the tmux copy-paste hint. Falls back to box-drawing if the font lacks Nerd Font ranges; layout is unaffected. - Visual differentiation: cyan + underline = clickable hyperlink; green = copy this command. Bump skill to v1.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Restore Nerd Font icons via Unicode escapes The copy glyph in `emit_tmux` was a literal Nerd Font byte embedded in the printf string and got stripped during a prior rewrite. Promote all three icons (rocket / graph / copy) to top-level shell variables that use \uHHHH-equivalent UTF-8 escapes, so the source survives editor round-trips that don't preserve Private Use Area code points. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/internal-dev-workbench] Rename from dev-tmux, set author + reset version - Rename `skills/dev-tmux/` → `skills/internal-dev-workbench/` to make the name self-explanatory about the skill's scope (an internal contributor's local dev workbench, not a generic tmux helper). - Author: Pranay Prakash. Version: 0.1 (first release of the skill). - Update internal references in SKILL.md and statusline.sh accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3535caf commit ab639a1

4 files changed

Lines changed: 308 additions & 0 deletions

File tree

.changeset/better-pets-reply.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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`).
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env bash
2+
# Claude Code statusline helper for the `internal-dev-workbench` skill.
3+
#
4+
# Reads `portless list` and emits a single line summarizing the active dev
5+
# session for the current git worktree:
6+
#
7+
# dev · obs · tmux attach -t <worktree-prefix>
8+
#
9+
# ` dev` and ` obs` are OSC 8 hyperlinks (clickable in any modern
10+
# terminal: iTerm2, Kitty, WezTerm, Terminal.app, Ghostty), styled bold +
11+
# underlined + bright cyan. The tmux fragment is shown when a session
12+
# named exactly the worktree prefix exists, in bold bright green to
13+
# signal "copy this" rather than "click this". Nerd Font glyphs are used
14+
# for the leading icons.
15+
#
16+
# Wire it into ~/.claude/settings.json with the path pointing at your
17+
# *primary* checkout — NOT a worktree, since worktrees get deleted:
18+
#
19+
# {
20+
# "statusLine": {
21+
# "type": "command",
22+
# "command": "$HOME/github/vercel/workflow/skills/internal-dev-workbench/statusline.sh"
23+
# }
24+
# }
25+
#
26+
# Worktree-aware: uses Claude's `workspace.current_dir` (stdin JSON) to
27+
# derive the current branch and filter portless routes / tmux sessions
28+
# to the active worktree. With no input or no matching session/routes,
29+
# the script prints nothing.
30+
31+
set -u
32+
33+
input=""
34+
if [ ! -t 0 ]; then
35+
input=$(cat)
36+
fi
37+
38+
cwd="${PWD}"
39+
if [ -n "$input" ] && command -v jq >/dev/null 2>&1; then
40+
parsed_cwd=$(printf '%s' "$input" | jq -r '.workspace.current_dir // empty' 2>/dev/null || true)
41+
[ -n "$parsed_cwd" ] && cwd="$parsed_cwd"
42+
fi
43+
44+
# Resolve the worktree's portless prefix (basename of the branch — same
45+
# convention `portless run` uses for linked worktrees, and the same name
46+
# the internal-dev-workbench skill assigns to its tmux session).
47+
prefix=""
48+
if command -v git >/dev/null 2>&1; then
49+
branch=$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
50+
[ -n "$branch" ] && [ "$branch" != "HEAD" ] && prefix="${branch##*/}"
51+
fi
52+
53+
# Portless routes (silent if portless is missing or has nothing).
54+
routes=""
55+
if command -v portless >/dev/null 2>&1; then
56+
routes=$(portless list 2>/dev/null || true)
57+
fi
58+
59+
pick_route() {
60+
local name="$1" url
61+
[ -z "$routes" ] && return
62+
if [ -n "$prefix" ]; then
63+
url=$(printf '%s\n' "$routes" \
64+
| awk -v p="$prefix" -v n="$name" \
65+
'$1 ~ ("https?://"p"\\."n"\\.localhost") {print $1; exit}')
66+
[ -n "$url" ] && { printf '%s' "$url"; return; }
67+
fi
68+
printf '%s\n' "$routes" \
69+
| awk -v n="$name" '$1 ~ ("https?://([^.]+\\.)?"n"\\.localhost") {print $1; exit}'
70+
}
71+
72+
dev_url=$(pick_route turbopack)
73+
obs_url=$(pick_route workflow-obs)
74+
75+
# Tmux session named exactly the worktree prefix.
76+
session=""
77+
if [ -n "$prefix" ] && command -v tmux >/dev/null 2>&1; then
78+
if tmux has-session -t "=$prefix" 2>/dev/null; then
79+
session="$prefix"
80+
fi
81+
fi
82+
83+
# Bail quietly if there's nothing to show.
84+
[ -z "$dev_url" ] && [ -z "$obs_url" ] && [ -z "$session" ] && exit 0
85+
86+
# OSC 8 hyperlink, styled bold + underlined + bright cyan so it reads as
87+
# a clickable link. Each emission resets its own styling so callers don't
88+
# need to re-establish color state.
89+
emit_link() {
90+
local url="$1" label="$2"
91+
printf '\033]8;;%s\033\\\033[1;4;96m%s\033[0m\033]8;;\033\\' "$url" "$label"
92+
}
93+
94+
# Bright green tmux command — visually distinct from the cyan-underline
95+
# links; signals "copy this" rather than "click this". The copy-glyph
96+
# icon is sourced from the ICON_CP variable defined below to avoid the
97+
# Nerd Font byte being stripped by editors that don't preserve the
98+
# Private Use Area.
99+
emit_tmux() {
100+
printf '\033[1;92m%s tmux attach -t %s\033[0m' "${ICON_CP}" "$1"
101+
}
102+
103+
# Bold separator so it reads at the same weight as the surrounding tokens.
104+
emit_sep() {
105+
printf '\033[1m · \033[0m'
106+
}
107+
108+
first=1
109+
sep() {
110+
if [ $first -eq 1 ]; then
111+
first=0
112+
else
113+
emit_sep
114+
fi
115+
}
116+
117+
# Nerd Font glyphs (octicon rocket, octicon graph, fa copy) embedded as
118+
# Unicode escapes so the source survives any encoding round-trip.
119+
ICON_DEV=$''
120+
ICON_OBS=$''
121+
ICON_CP=$''
122+
123+
if [ -n "$dev_url" ]; then
124+
sep
125+
emit_link "$dev_url" "${ICON_DEV} dev"
126+
fi
127+
if [ -n "$obs_url" ]; then
128+
sep
129+
emit_link "$obs_url" "${ICON_OBS} obs"
130+
fi
131+
if [ -n "$session" ]; then
132+
sep
133+
emit_tmux "$session"
134+
fi
135+
136+
printf '\n'

workbench/nextjs-turbopack/next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ const turbopackRoot = path.resolve(process.cwd(), '../..');
77
const nextConfig: NextConfig = {
88
/* config options here */
99
serverExternalPackages: ['@node-rs/xxhash'],
10+
// Allow portless-style worktree-prefixed .localhost subdomains (e.g.
11+
// https://<branch>.turbopack.localhost) so HMR and dev-only endpoints
12+
// aren't blocked by Next's cross-origin protection in dev.
13+
allowedDevOrigins: ['turbopack.localhost', '*.turbopack.localhost'],
1014
turbopack: {
1115
// Keep Turbopack root aligned with repo root so @repo/* path aliases can
1216
// resolve files outside the app directory in both monorepo and staged temp layouts.

0 commit comments

Comments
 (0)