Skip to content

Commit d05ce68

Browse files
committed
Merge remote-tracking branch 'origin/main' into published-caching-cs-11043
2 parents 96335bc + 142842f commit d05ce68

129 files changed

Lines changed: 9986 additions & 1552 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude-plugin/marketplace.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "cardstack-boxel",
3+
"owner": {
4+
"name": "Cardstack",
5+
"url": "https://boxel.ai"
6+
},
7+
"description": "Claude Code plugins published from the cardstack/boxel monorepo.",
8+
"plugins": [
9+
{
10+
"name": "boxel-cli",
11+
"source": "./packages/boxel-cli/plugin",
12+
"description": "Skills for working with Boxel realms via @cardstack/boxel-cli."
13+
}
14+
]
15+
}

.claude/skills/indexing-diagnostics/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,8 +1220,8 @@ Three env vars control the per-prerender-server shape. They're resolved once at
12201220

12211221
| Env var | Default | What it controls | When to change it |
12221222
|---|---|---|---|
1223-
| `PRERENDER_PAGE_POOL_SIZE` | `5` | Total simultaneous Chrome tabs the pool can manage. Also the `capacity` the server reports to the manager on each heartbeat, which drives warm-vacancy routing. | Fleet capacity. Raise when `waits.semaphoreMs` dominates `launchMs` across rows from all realms (server-wide saturation); lower if you need to reduce memory footprint and you can confirm from snapshots that pending rarely approaches `totalTabs`. |
1224-
| `PRERENDER_AFFINITY_TAB_MAX` | `5` (clamped to `PRERENDER_PAGE_POOL_SIZE`) | Max tabs a single affinity (realm or user) can simultaneously hold from the pool. | Rarely. Must be ≥ 2 for the self-referential prerender deadlock to be prevented — PagePool logs a warning at startup when it isn't. Lower only if you want to force multi-realm fairness at the tab-routing level. |
1223+
| `PRERENDER_PAGE_POOL_MIN` / `_MAX` | unset → fixed pool of `options.maxPages` (5) | Dynamic-pool envelope. The pool boots at MIN, expands up to MAX under saturation, contracts back to MIN after sustained idle. The live capacity is what the server reports to the manager on each heartbeat, which drives warm-vacancy routing. | Fleet capacity. Raise MAX when `waits.semaphoreMs` dominates `launchMs` across rows from all realms (server-wide saturation); lower MAX if you need to reduce memory footprint and you can confirm from snapshots that pending rarely approaches `totalTabs`. Setting MIN === MAX disables expansion/contraction. |
1224+
| `PRERENDER_AFFINITY_TAB_MAX` | `5` (clamped to the effective pool max: `PRERENDER_PAGE_POOL_MAX` when set, otherwise fixed `maxPages`) | Max tabs a single affinity (realm or user) can simultaneously hold from the pool. | Rarely. Must be ≥ 2 for the self-referential prerender deadlock to be prevented — PagePool logs a warning at startup when it isn't. Lower only if you want to force multi-realm fairness at the tab-routing level. |
12251225
| `PRERENDER_AFFINITY_FILE_CONCURRENCY` | unset → `max(1, PRERENDER_AFFINITY_TAB_MAX − 1)` (the deadlock-safety ceiling) | Cap on concurrent `file` renders within a single affinity. Module and command calls bypass admission; they're never capped by this knob. | Cross-realm fairness. When one realm's fan-out (e.g. a catalog reindex) is stealing render budget from every other realm, lower this below the ceiling to reserve tabs for other affinities. The effective cap is always `min(env, ceiling)` so this can't accidentally break the deadlock-safety invariant. |
12261226

12271227
**Default invariant**: when `PRERENDER_AFFINITY_FILE_CONCURRENCY` is unset, the effective file-admission cap equals the deadlock-safety ceiling — same behavior as before the knob existed. Changing the knob is an explicit operator decision driven by `admissionMs` telemetry; don't adjust it without data.

.github/workflows/ci-lint.yaml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,100 @@ jobs:
125125
if: ${{ !cancelled() }}
126126
run: pnpm run lint
127127
working-directory: packages/boxel-cli
128+
- name: Verify Boxel CLI plugin synopsis is fresh
129+
# Regenerates the `<!-- generated:commands -->` blocks in plugin/skills/*/SKILL.md
130+
# from the Commander tree and fails if the working tree changed — i.e. someone
131+
# added or changed a CLI command without running `pnpm build:plugin`.
132+
if: ${{ !cancelled() }}
133+
run: |
134+
pnpm run build:plugin
135+
if ! git diff --exit-code -- plugin/skills; then
136+
echo "::error::plugin/skills synopsis is stale. Run 'pnpm build:plugin' in packages/boxel-cli and commit the result."
137+
exit 1
138+
fi
139+
working-directory: packages/boxel-cli
140+
- name: Verify boxel-skills sync
141+
# Regenerates plugin/skills/boxel-development and plugin/skills/boxel-design from
142+
# cardstack/boxel-skills at the pinned tag in scripts/build-skills.ts. Fails if
143+
# the working tree changed — i.e. someone hand-edited boxel-skills-derived
144+
# content without bumping the pin and re-running `pnpm build:skills`.
145+
if: ${{ !cancelled() }}
146+
run: |
147+
pnpm run build:skills
148+
if ! git diff --exit-code -- plugin/skills; then
149+
echo "::error::plugin/skills is out of sync with cardstack/boxel-skills. Bump BOXEL_SKILLS_VERSION in scripts/build-skills.ts (or fix upstream), run 'pnpm build:skills' in packages/boxel-cli, and commit the result."
150+
exit 1
151+
fi
152+
working-directory: packages/boxel-cli
153+
- name: Verify plugin version bumped when synopsis changed
154+
# If a PR's diff against main touches any generated:commands block, the plugin's
155+
# version must also bump in the same diff. Otherwise marketplace consumers won't
156+
# see the update — Claude Code caches by version string.
157+
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
158+
run: |
159+
set -euo pipefail
160+
BASE="${{ github.event.pull_request.base.sha }}"
161+
HEAD="${{ github.event.pull_request.head.sha }}"
162+
# `actions/checkout` uses default depth, so $BASE (a `main` SHA) usually
163+
# isn't local. Fetch both explicitly so `git diff`/`git show` succeed.
164+
git fetch --no-tags --depth=1 origin "$BASE" "$HEAD"
165+
# Did the generated-commands block change in any SKILL.md? Compare the
166+
# block at $BASE vs $HEAD per-file. Any difference inside the markers
167+
# (heading, description, argument, blank line) flips SYNOPSIS_CHANGED.
168+
# `git cat-file -e` is the explicit existence probe — using `git show`
169+
# with `2>/dev/null` would still propagate exit 128 and trip pipefail.
170+
extract_block() {
171+
local rev="$1" path="$2"
172+
if git cat-file -e "$rev:$path" 2>/dev/null; then
173+
git show "$rev:$path" \
174+
| awk '/<!-- generated:commands:start -->/,/<!-- generated:commands:end -->/'
175+
fi
176+
}
177+
SYNOPSIS_CHANGED=
178+
while IFS= read -r f; do
179+
base_block=$(extract_block "$BASE" "$f")
180+
head_block=$(extract_block "$HEAD" "$f")
181+
if [ "$base_block" != "$head_block" ]; then
182+
SYNOPSIS_CHANGED=1
183+
break
184+
fi
185+
done < <(git ls-tree -r --name-only "$HEAD" -- packages/boxel-cli/plugin/skills | grep '/SKILL\.md$' || true)
186+
if [ -z "$SYNOPSIS_CHANGED" ]; then
187+
echo "No generated synopsis changes detected; skipping version-bump check."
188+
exit 0
189+
fi
190+
# Was the plugin manifest version touched?
191+
if ! git diff "$BASE" "$HEAD" -- packages/boxel-cli/plugin/.claude-plugin/plugin.json \
192+
| grep -qE '^[+-]\s*"version"'; then
193+
echo "::error::plugin/skills synopsis changed but plugin.json version was not bumped. Marketplace consumers won't see the update without a new version. Bump 'version' in packages/boxel-cli/plugin/.claude-plugin/plugin.json."
194+
exit 1
195+
fi
196+
echo "Synopsis changed and plugin.json version was bumped — OK."
197+
working-directory: .
198+
- name: Verify plugin version bumped when boxel-skills content changed
199+
# Mirrors the synopsis-bump check above, but gates on changes to skill folders
200+
# generated by `pnpm build:skills` (boxel-development/, boxel-design/). Any
201+
# content change there requires a plugin.json bump so marketplace consumers
202+
# actually pick up the new content — Claude Code caches by version string.
203+
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
204+
run: |
205+
set -euo pipefail
206+
BASE="${{ github.event.pull_request.base.sha }}"
207+
HEAD="${{ github.event.pull_request.head.sha }}"
208+
# See note in the previous step — fetch base/head explicitly because
209+
# `actions/checkout` uses default depth and $BASE often isn't local.
210+
git fetch --no-tags --depth=1 origin "$BASE" "$HEAD"
211+
SKILLS_CHANGED=$(git diff --name-only "$BASE" "$HEAD" -- \
212+
'packages/boxel-cli/plugin/skills/boxel-development/**' \
213+
'packages/boxel-cli/plugin/skills/boxel-design/**')
214+
if [ -z "$SKILLS_CHANGED" ]; then
215+
echo "No boxel-skills-derived changes detected; skipping version-bump check."
216+
exit 0
217+
fi
218+
if ! git diff "$BASE" "$HEAD" -- packages/boxel-cli/plugin/.claude-plugin/plugin.json \
219+
| grep -qE '^[+-]\s*"version"'; then
220+
echo "::error::boxel-skills-derived content changed but plugin.json version was not bumped. Marketplace consumers won't see the update without a new version. Bump 'version' in packages/boxel-cli/plugin/.claude-plugin/plugin.json."
221+
exit 1
222+
fi
223+
echo "boxel-skills content changed and plugin.json version was bumped — OK."
224+
working-directory: .

.github/workflows/manual-deploy.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
echo "environment_url=" >> "$GITHUB_OUTPUT"
3838
fi
3939
- id: create
40-
uses: actions/github-script@v7
40+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
4141
with:
4242
script: |
4343
const environment = '${{ inputs.environment }}';
@@ -54,7 +54,7 @@ jobs:
5454
});
5555
core.setOutput('deployment_id', response.data.id.toString());
5656
- name: Mark deployment in progress
57-
uses: actions/github-script@v7
57+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
5858
with:
5959
script: |
6060
const deployment_id = Number('${{ steps.create.outputs.deployment_id }}');
@@ -360,7 +360,7 @@ jobs:
360360
runs-on: ubuntu-latest
361361
steps:
362362
- name: Set deployment status
363-
uses: actions/github-script@v7
363+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
364364
env:
365365
NEEDS: ${{ toJson(needs) }}
366366
DEPLOYMENT_ID: ${{ needs.create-deployment.outputs.deployment-id }}

.github/workflows/observability-diff.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,10 @@ jobs:
165165
echo "::endgroup::"
166166
167167
- name: Post sticky PR comment
168-
# Pinned to SHA (rather than @v7) because this step has
168+
# Pinned to SHA (rather than @v9) because this step has
169169
# pull-requests: write and runs after assuming an AWS role —
170170
# supply-chain risk is non-negligible.
171-
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
171+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
172172
env:
173173
DIFF_FILE: ${{ steps.diff.outputs.diff_file }}
174174
with:

.mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tools]
22
node = "24.13.1"
3-
pnpm = "10.30.0"
3+
pnpm = "10.33.4"
44

55
[env]
66
_.source = "./mise-tasks/lib/env-vars.sh"

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ This sets the following defaults (all individually overridable):
158158
| Variable | Normal | Turbo |
159159
| ---------------------------- | ------ | ----- |
160160
| `PRERENDER_COUNT` | 1 | 3 |
161-
| `PRERENDER_PAGE_POOL_SIZE` | 4 | 4 |
161+
| `PRERENDER_PAGE_POOL_MIN` | 4 | 4 |
162+
| `PRERENDER_PAGE_POOL_MAX` | 4 | 4 |
162163
| `WORKER_HIGH_PRIORITY_COUNT` | 0 | 4 |
163164
| `WORKER_ALL_PRIORITY_COUNT` | 1 | 4 |
164165

@@ -221,7 +222,7 @@ In environment mode, this is at `http://worker.environment-name.localhost/_index
221222
Boxel supports server-side rendering of cards via a lightweight prerender service and an optional manager that coordinates multiple services.
222223

223224
- Prerender server: Handles POST `/prerender-card` (cards) and `/prerender-module` (modules) requests that include user/session permissions and a target URL. It launches a headless browser and maintains a small pool of per-realm pages (LRU-evicted) to speed up subsequent renders. Each page keeps a logged-in session for its realm and reuses the page for repeated renders of that realm.
224-
- Pooling: Each prerender server maintains up to PRERENDER_PAGE_POOL_SIZE pages (default 4). When the pool is full, the least-recently-used realm is evicted (its browser context is closed). When a page becomes unusable (timeout or explicit unusable signal), the realm is evicted proactively.
225+
- Pooling: Each prerender server maintains a dynamic page pool sized between `PRERENDER_PAGE_POOL_MIN` and `PRERENDER_PAGE_POOL_MAX` (both default 4 in dev). When the pool is full, the least-recently-used realm is evicted (its browser context is closed). When a page becomes unusable (timeout or explicit unusable signal), the realm is evicted proactively.
225226
- Prerender manager: When multiple prerender servers are running, a central manager receives `/prerender-card` and `/prerender-module` requests and routes them to a suitable server. The manager tracks which servers are registered and which realms they actively handle. It supports realm affinity, multiplexing the same realm across multiple servers to handle high prerender throughput, capacity-aware selection, and health-based eviction of unreachable servers.
226227

227228
#### Pre-rendering start scripts
@@ -249,8 +250,9 @@ Prerender server:
249250
- BOXEL_HOST_URL (optional): URL of the host app that serves the /render routes. Defaults to http://localhost:4200 in dev scripts.
250251
- PRERENDER_MANAGER_URL (optional): Base URL of the prerender manager to register with. Defaults to http://localhost:4222.
251252
- PRERENDER_COUNT (optional): Number of prerender server instances to start. Each gets its own headless Chrome. Default 1.
252-
- PRERENDER_PAGE_POOL_SIZE (optional): Max number of per-realm pages to keep open in the pool. Default 4.
253-
- PRERENDER_AFFINITY_TAB_MAX (optional): Max number of tabs a single realm can use within a prerender server. Defaults to PRERENDER_PAGE_POOL_SIZE (i.e. a realm can use all available tabs).
253+
- PRERENDER_PAGE_POOL_MIN (optional): Idle floor for the dynamic page pool. The pool boots at this size and contracts back to it after sustained idle. Default 4 in dev (set in `mise-tasks/lib/env-vars.sh`).
254+
- PRERENDER_PAGE_POOL_MAX (optional): Burst ceiling for the dynamic page pool. The pool expands up to this size under saturation. Default 4 in dev. Setting MIN === MAX disables expansion/contraction (fixed-size pool).
255+
- PRERENDER_AFFINITY_TAB_MAX (optional): Max number of tabs a single realm can use within a prerender server. Defaults to 5, clamped to the effective pool max (`PRERENDER_PAGE_POOL_MAX` when set, otherwise the fixed `maxPages` pool size).
254256
- BOXEL_SHOW_PRERENDER (optional): If set to 'true', opens a visible browser (useful for debugging locally). Headless otherwise.
255257

256258
Prerender manager:

docs/aws-operations.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Conventions used throughout:
1414

1515
## Activate the dynamic-pool prerender server
1616

17-
Flips the boxel prerender server's `PagePool` from legacy fixed-size capacity (driven by `PRERENDER_PAGE_POOL_SIZE`) to the dynamic-pool envelope (driven by `MIN` / `MAX` / `HIGH_PRIORITY_MAX`). Use this once the dynamic-pool code is deployed and you're ready to activate the new behaviour on a deployed environment.
17+
Sets the boxel prerender server's `PagePool` envelope (`MIN` / `MAX` / `HIGH_PRIORITY_MAX`). Use this when first activating dynamic-pool behaviour on a deployed environment, or when re-tuning the envelope after a workload shift.
1818

1919
### Pre-requisites
2020

@@ -98,24 +98,21 @@ If the ECS task is still 4 vCPU / 8 GB, the recommended values would OOM at `HP_
9898

9999
### Rollback
100100

101-
Restoring legacy fixed-pool behaviour without a code change:
101+
Restore the previous envelope values via SSM, then force-redeploy. Capture the pre-change values before applying new ones so rollback is a single `put-parameter` per knob — there is no longer a "fall back to a different env var" escape hatch, so the rollback target must be a known-good envelope.
102102

103103
```sh
104-
# Reset all dynamic-pool knobs to the SSM placeholder value "0"
105-
# (which the application code treats as "unset", falling back to
106-
# the legacy PRERENDER_PAGE_POOL_SIZE path).
107-
for name in PRERENDER_PAGE_POOL_MIN PRERENDER_PAGE_POOL_MAX \
108-
PRERENDER_PAGE_POOL_HIGH_PRIORITY_MAX \
109-
PRERENDER_HIGH_PRIORITY_THRESHOLD \
110-
PRERENDER_POOL_IDLE_CONTRACTION_MS \
111-
PRERENDER_SHARED_CONTEXT_CAP; do
112-
aws --profile $PROFILE ssm put-parameter \
113-
--name "/${ENV}/boxel/${name}" --value "0" --overwrite
114-
done
104+
# Example: restoring a previous envelope. Substitute the values you
105+
# captured before applying.
106+
aws --profile $PROFILE ssm put-parameter \
107+
--name "/${ENV}/boxel/PRERENDER_PAGE_POOL_MIN" --value <prev-min> --overwrite
108+
aws --profile $PROFILE ssm put-parameter \
109+
--name "/${ENV}/boxel/PRERENDER_PAGE_POOL_MAX" --value <prev-max> --overwrite
110+
# … repeat for PRERENDER_PAGE_POOL_HIGH_PRIORITY_MAX,
111+
# PRERENDER_HIGH_PRIORITY_THRESHOLD,
112+
# PRERENDER_POOL_IDLE_CONTRACTION_MS,
113+
# PRERENDER_SHARED_CONTEXT_CAP as needed.
115114

116115
aws --profile $PROFILE ecs update-service \
117116
--cluster $ENV --service boxel-prerender-server-$ENV \
118117
--force-new-deployment
119118
```
120-
121-
The pool re-enters the legacy behaviour driven by `PRERENDER_PAGE_POOL_SIZE` once the rollout finishes.

mise-tasks/dev

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@
1313
set -m
1414

1515
cleanup() {
16-
trap - EXIT INT TERM
16+
trap - EXIT INT TERM HUP
1717
if [ -n "${SAT_PID:-}" ]; then
1818
kill_tree "$SAT_PID"
1919
fi
2020
sweep_orphaned_services
2121
}
22-
trap cleanup EXIT INT TERM
22+
# HUP is required: when the user hits Ctrl-C in a terminal running `mise run
23+
# dev`, mise often exits without forwarding SIGINT to this bash script, and
24+
# bash then receives SIGHUP from the dying parent. Bash's default action on
25+
# an untrapped SIGHUP is to terminate *without* running the EXIT trap, so
26+
# without `HUP` here the cleanup never fires and the entire subtree leaks.
27+
trap cleanup EXIT INT TERM HUP
2328

2429
WAIT_ON_TIMEOUT=7200000 NODE_NO_WARNINGS=1 start-server-and-test \
2530
'run-p -ln start:pg start:matrix start:smtp start:prerender-dev start:prerender-manager-dev start:worker-development start:development' \

mise-tasks/dev-all

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ set -m
1818
pnpm --filter @cardstack/host start &
1919
HOST_PID=$!
2020
cleanup() {
21-
trap - EXIT INT TERM
21+
trap - EXIT INT TERM HUP
2222
if [ -n "${SAT_PID:-}" ]; then
2323
kill_tree "$SAT_PID"
2424
fi
2525
kill_tree "$HOST_PID"
2626
sweep_orphaned_services
2727
}
28-
trap cleanup EXIT INT TERM
28+
# HUP is required: when the user hits Ctrl-C in a terminal running `mise run
29+
# dev-all`, mise often exits without forwarding SIGINT to this bash script,
30+
# and bash then receives SIGHUP from the dying parent. Bash's default action
31+
# on an untrapped SIGHUP is to terminate *without* running the EXIT trap, so
32+
# without `HUP` here the cleanup never fires and the entire subtree leaks.
33+
trap cleanup EXIT INT TERM HUP
2934

3035
HOST_TIMEOUT=120
3136
ELAPSED=0

0 commit comments

Comments
 (0)