Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ced7bab
fix(release): the ship job publishes through npm Trusted Publishing (…
jonit-dev Nov 3, 2026
09cf246
fix(release): wait for the install packument before the clean room, s…
jonit-dev Nov 3, 2026
33c6924
fix(release): packages name threenative-engine, and the clean room wa…
jonit-dev Nov 3, 2026
e9707dd
fix(ci): the scope job checks out only scripts/ and the kit files
jonit-dev Nov 3, 2026
5e030ca
PRD-539: try Unreal TSR history levers and benchmark against Unreal (…
jonit-dev Nov 4, 2026
2f0e94f
Every template runs on the native engine; TSL compute; CI cache, rele…
jonit-dev Nov 4, 2026
6a9fb16
PRD-632: expose and tone-map the frame like a fresh Unreal project (#…
jonit-dev Nov 4, 2026
d692aaa
CI caches stop missing on test-native and Android, and the GI harness…
jonit-dev Nov 4, 2026
f129102
feat(spells): the Ultra Elements Fireball, copied and run on our engi…
jonit-dev Nov 4, 2026
7fcd31c
fix(runtime-native): a game with no asset files builds for desktop (#…
jonit-dev Nov 5, 2026
1cb51d0
fix(runtime-native): the desktop bundler handles Windows paths (#612)…
jonit-dev Nov 5, 2026
11bbfb5
The Windows engine player starts: no more 0xC0000409 crash before mai…
jonit-dev Nov 5, 2026
8604177
feat(authoring): mechanics discovery watches gameplay frames of the r…
jonit-dev Nov 5, 2026
44ca799
fix(docs): the agent interface names no package it does not ship (#617)
jonit-dev Nov 5, 2026
0a98178
A material can choose its own lights on the Wasm engine (lightsNode) …
jonit-dev Nov 6, 2026
972d1e3
EPIC: Strata terrain hardening (#483)
jonit-dev Nov 6, 2026
3e934a1
The GI calibration harness stops subtracting a diffuse loss Unreal co…
jonit-dev Nov 6, 2026
0592ccd
chore(runtime-native): regenerate the native coverage report after th…
jonit-dev Nov 6, 2026
79b8ade
chore(release): prepare the 0.3.12 cohort
jonit-dev Nov 6, 2026
a97c528
chore(ci): scope a gitleaks false positive on PRD-595 prose
jonit-dev Nov 6, 2026
3ec77c9
fix(ci): the benchmark job downloads the built packages its Strata ba…
jonit-dev Nov 6, 2026
66d0b81
fix(ci): the Strata startup verifier reads the 1.8 MB dependency tree…
jonit-dev Nov 6, 2026
86d753d
Kit agents get their tools: ship no junk, load and gate the MCP serve…
jonit-dev Nov 6, 2026
9753447
fix: animation MCP pins follow the 0.1.9 cohort
jonit-dev Nov 6, 2026
3cc899c
fix(ci): the Strata startup gate compares against the merged epic and…
jonit-dev Nov 6, 2026
9093092
fix(core): a mesh that casts in neither state no longer redraws cache…
jonit-dev Nov 6, 2026
74a1308
fix(ci): strata is owed only when the run reaches a GPU runner, and t…
jonit-dev Nov 6, 2026
465259c
fix(ci): the benchmark Strata startup contract runs only where the jo…
jonit-dev Nov 6, 2026
a2f956b
fix(ci): the plan records a GPU-less strata exemption so caller repla…
jonit-dev Nov 6, 2026
2b6ad4c
fix(ci): a promotion qualifies with the recorded GPU-less strata exem…
jonit-dev Nov 6, 2026
4c12001
fix(engine-mcp): a failed watch removes its frame folder, and the wat…
jonit-dev Nov 6, 2026
7bab69b
fix(abyss-framework): the WorldProbe camera keeps its gaze on the map…
jonit-dev Nov 6, 2026
45a6482
fix(core): the WorldProbe gaze test reads the camera vectors as tuple…
jonit-dev Nov 6, 2026
25bed77
fix(core): the filmic output dither draws integer PCG grain, so web a…
jonit-dev Nov 6, 2026
3a24645
fix(runtime-native): four dithered parity cases take a measured 0.03 …
jonit-dev Nov 6, 2026
be7c082
fix(runtime-native): the virtual-shadow first-frame pin follows the m…
jonit-dev Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .agents/skills/unreal-calibration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ One epic is one draft PR, and it is never merged during the run. A coordinator s
- **Captures are untracked.** Specs read the committed `gi/<scene>/id` and `scene.json`, so `compare --check` needs the captures in the worktree that runs it.
- **A lane that rebuilds a calibration scene** tells the judge or lock owner in the same hour, because every lock on that scene goes stale.
- **Parallel captures in one worktree** race on the package builds. Run them one after another.
- **Two lanes in one capture work dir fail with `TN_VISUAL_PACKAGES_MISSING`.** `~/.cache/threenative/unreal-look` is shared by every lane on the machine; give each lane its own `TN_UNREAL_LOOK_WORK`.
- **Unreal triangle counts:** `get_num_triangles(0)` reads the Nanite fallback mesh. Read the source mesh description for the real count.
- **"No fresh binary" is not a blocker.** Build the host in your own worktree.

Expand Down
6 changes: 0 additions & 6 deletions .claude/settings.local.json

This file was deleted.

6 changes: 4 additions & 2 deletions .claude/skills/capture-strata-worlds/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ GPU give comparable frames. A visual change still needs the visual judge subagen

1. Build the example from the repo root:
`pnpm --filter strata-terrain-preview build`
2. Serve the build on loopback:
`python3 -m http.server 8092 --bind 127.0.0.1 -d examples/strata-terrain-preview/dist &`
2. Serve the build on loopback with `vite preview`. Its middleware serves the asset library
(spruce GLBs, rockface textures, the HDR); a static server returns 404 for them and the frames
miss those assets:
`pnpm --filter strata-terrain-preview exec vite preview --host 127.0.0.1 --port 8092 --strictPort &`
3. Capture (headed, real display):
`DISPLAY=:0 node .claude/skills/capture-strata-worlds/shoot-worlds.mjs http://127.0.0.1:8092 <outDir>`
Add `--worlds alpine,desert` for a subset, and `--repo <path>` when the served build came from another worktree, so `report.json` records that checkout's commit.
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/unreal-calibration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ One epic is one draft PR, and it is never merged during the run. A coordinator s
- **Captures are untracked.** Specs read the committed `gi/<scene>/id` and `scene.json`, so `compare --check` needs the captures in the worktree that runs it.
- **A lane that rebuilds a calibration scene** tells the judge or lock owner in the same hour, because every lock on that scene goes stale.
- **Parallel captures in one worktree** race on the package builds. Run them one after another.
- **Two lanes in one capture work dir fail with `TN_VISUAL_PACKAGES_MISSING`.** `~/.cache/threenative/unreal-look` is shared by every lane on the machine; give each lane its own `TN_UNREAL_LOOK_WORK`.
- **Unreal triangle counts:** `get_num_triangles(0)` reads the Nanite fallback mesh. Read the source mesh description for the real count.
- **"No fresh binary" is not a blocker.** Build the host in your own worktree.

Expand Down
141 changes: 121 additions & 20 deletions .github/actions/android-v8-source/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,38 @@ outputs:
published:
description: "'true' when the payload came from the published release asset."
value: ${{ steps.published.outputs.hit }}
built:
description: >-
'true' only when this run compiled the payload. A payload restored from an artifact is
somebody else's bytes, so it is never published as the trusted release asset.
value: ${{ steps.build.outputs.built }}
trusted:
description: >-
'true' only when this run compiled the payload, or restored it from the Actions cache on a
push. A pull request's cache holds pull request code and an artifact is another run's bytes,
so neither may become the release asset; a trusted ref's cache is that ref's own payload.
value: >-
${{ steps.build.outputs.built == 'true' ||
(github.event_name != 'pull_request' && steps.payload.outputs.cache-hit == 'true') }}

runs:
using: composite
steps:
# The recipe decides whether a cached payload is still valid. Compute it once here so every
# cache key below agrees, and leave this action's own plumbing out of the inputs: editing the
# workflow that restores V8 must not throw away a built V8.
# workflow that restores V8 must not throw away a built V8. The script hashes the `v8-android`
# entry of the provisioner alone, so an unrelated dependency edit in that file keeps the key
# (PRD-628); hashing the whole file is what cold-built V8 in run 39184795139.
- name: Compute the pinned V8 recipe hash
id: recipe
shell: bash
env:
RECIPE_HASH: >-
${{ hashFiles(
'packages/runtime-native/scripts/build-android-v8.mjs',
'packages/runtime-native/scripts/check-android-16kb-alignment.mjs',
'packages/runtime-native/scripts/download-deps.mjs',
'packages/runtime-native/android/gradle.properties') }}
run: echo "hash=$RECIPE_HASH" >> "$GITHUB_OUTPUT"
# The hash names the release asset and every cache key below, so a node failure that an
# `echo` swallowed would publish the payload under an empty name and miss every later run.
run: |
set -euo pipefail
hash=$(node "${{ inputs.runtime-root }}/scripts/v8-recipe-hash.mjs")
[ -n "$hash" ] || { echo "::error::the V8 recipe hash script printed nothing"; exit 1; }
echo "hash=$hash" >> "$GITHUB_OUTPUT"
# The durable copy. The Actions cache alone could not hold the payload: the repository's 10 GiB
# budget is shared by every lane, and on 2026-09-28 the nightly's payload (saved 12:06) was
# evicted before 15:48, so every PR cold-built V8 for two hours. It was also branch-scoped: a
Expand All @@ -53,22 +67,92 @@ runs:
tar -xzf "$RUNNER_TEMP/$ASSET" -C "$DEST"
echo "hit=true" >> "$GITHUB_OUTPUT"
else
echo "No published $ASSET; falling back to the Actions cache and the source build."
echo "No published $ASSET; falling back to the artifact, the Actions cache and the source build."
echo "hit=false" >> "$GITHUB_OUTPUT"
fi
# The store every ref can read. An Actions cache is scoped to the ref that saved it: a payload a
# `workflow_dispatch` or push run on the head branch saved is invisible to that branch's pull
# request runs, because a pull request reads only its own merge ref and its base branch (PRD-628
# finding 2). A workflow artifact has no ref scope, so a pull request reads a payload any run of
# its head branch built — the dispatch-then-push case included.
#
# Any run can upload an artifact, a fork's pull request run included, and the payload's receipt
# is written by whoever built it. So only a pull request or a merge group reads this store, `v8-artifact-pick.mjs`
# admits only this repository's own head on main, develop or this pull request's branch, and a
# payload that fails `--verify` is thrown away instead of failing the lane. A trusted ref never
# reads it at all: it has the release asset, and a poisoned payload must not reach the job that
# publishes one (`trusted` below is what that job requires, and an artifact never sets it).
#
# A merge group reads it too, from its pull request's own head branch and its own queue branch.
# A pull request that changes the recipe has no release asset until develop publishes one after
# the merge, and a merge group cannot restore the pull request's cache, so its V8 built cold
# for over two hours and the queue dropped PR #605 at its 120-minute check timeout
# (`checks_timed_out`, run 41734911462) with every finished job green. The merge group runs the
# code that pull request run built, and the same picker and `--verify` still apply.
- name: Download the payload a pull request run of this recipe published
id: artifact
if: steps.published.outputs.hit != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group')
shell: bash
env:
GH_TOKEN: ${{ github.token }}
NAME: v8-android-${{ steps.recipe.outputs.hash }}
ROOT: ${{ inputs.runtime-root }}
DEST: ${{ inputs.runtime-root }}/third_party/v8-android
BRANCHES: main develop ${{ github.head_ref }}
run: |
set -euo pipefail
: "${ROOT:?}" "${DEST:?}" "${RUNNER_TEMP:?}"
if [ "${GITHUB_EVENT_NAME:-}" = merge_group ]; then
# `gh-readonly-queue/<base>/pr-<n>-<sha>` names the pull request. A fork's head is never
# added, and a failed lookup leaves main, develop and the queue branch.
pr=$(sed -nE 's#^gh-readonly-queue/[^/]+/pr-([0-9]+)-.*#\1#p' <<<"${GITHUB_REF_NAME:-}")
head=""
if [ -n "$pr" ]; then
head=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$pr" \
--jq "select(.head.repo.full_name == \"$GITHUB_REPOSITORY\") | .head.ref" 2>/dev/null) || head=""
fi
BRANCHES="$BRANCHES $GITHUB_REF_NAME $head"
fi
if run_id=$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=$NAME&per_page=100" \
| node "$ROOT/scripts/v8-artifact-pick.mjs" --branches "$BRANCHES"); then
:
else
# A missing read grant or an unreadable list is not a reason to fail the lane.
echo "::warning::could not list the artifacts named $NAME; cold-building instead"
run_id=""
fi
artifact_dir="$RUNNER_TEMP/v8-artifact"
rm -rf "$artifact_dir"
if [ -n "$run_id" ] && gh run download "$run_id" --repo "$GITHUB_REPOSITORY" \
--name "$NAME" --dir "$artifact_dir"; then
mkdir -p "$DEST"
cp -a "$artifact_dir/." "$DEST/"
if ( cd "$ROOT" && node scripts/build-android-v8.mjs --verify ); then
rm -rf "$artifact_dir"
echo "Restored the payload run $run_id published for $NAME."
echo "hit=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# An artifact that vanished between the list and the download, or that carries a payload
# failing its own receipt check, is a cold build — never a failed lane.
echo "::warning::the payload run $run_id published for $NAME failed its receipt check; cold-building instead"
fi
rm -rf "$artifact_dir" "$DEST"
echo "No usable artifact named $NAME; falling back to the Actions cache and the source build."
echo "hit=false" >> "$GITHUB_OUTPUT"
# The built payload is the expensive thing and it is stable per recipe. Its key carries no run
# id, so every run of the same recipe restores it instead of cold-building V8 into the timeout.
- name: Restore the pinned Android V8 payload
id: payload
if: steps.published.outputs.hit != 'true'
if: steps.published.outputs.hit != 'true' && steps.artifact.outputs.hit != 'true'
uses: actions/cache/restore@v4
with:
path: ${{ inputs.runtime-root }}/third_party/v8-android
key: android-v8-payload-${{ runner.os }}-${{ runner.arch }}-${{ steps.recipe.outputs.hash }}
# Only a payload miss needs the resumable source state. Its key keeps a per-run suffix because
# a resumed build must save to a unique key rather than collide with the run that produced it.
- name: Restore the resumable V8 source state
if: steps.published.outputs.hit != 'true' && steps.payload.outputs.cache-hit != 'true'
if: steps.published.outputs.hit != 'true' && steps.artifact.outputs.hit != 'true' && steps.payload.outputs.cache-hit != 'true'
uses: actions/cache/restore@v4
with:
path: |
Expand All @@ -82,7 +166,7 @@ runs:
${{ steps.recipe.outputs.hash }}-
- name: Build the pinned Android V8 payload
id: build
if: steps.payload.outputs.cache-hit != 'true'
if: steps.published.outputs.hit != 'true' && steps.artifact.outputs.hit != 'true' && steps.payload.outputs.cache-hit != 'true'
working-directory: ${{ inputs.runtime-root }}
shell: bash
run: |
Expand All @@ -98,31 +182,48 @@ runs:
export THREENATIVE_V8_BUILD_JOBS="${THREENATIVE_V8_BUILD_JOBS:-$(nproc)}"
timeout --signal=TERM --kill-after=30s 180m \
node scripts/download-deps.mjs --only v8-android
# This run compiled the payload. `built` states that fact, and `trusted` (the action's
# output) is what `publish-android-v8` in native-platforms.yml requires: a downloaded
# payload never becomes the trusted release asset.
echo "built=true" >> "$GITHUB_OUTPUT"
# Payload-only verification is valid: `--verify` inspects the installed payload and its receipt
# under third_party/v8-android and never reads third_party/.v8-source.
- name: Verify the complete Android V8 payload
id: verify
if: steps.payload.outputs.cache-hit == 'true' || steps.build.outcome == 'success'
if: steps.payload.outputs.cache-hit == 'true' || steps.artifact.outputs.hit == 'true' || steps.build.outcome == 'success'
working-directory: ${{ inputs.runtime-root }}
shell: bash
run: |
set -euo pipefail
node scripts/build-android-v8.mjs --verify
- name: Save the prebuilt Android V8 payload
if: steps.published.outputs.hit != 'true' && steps.payload.outputs.cache-hit != 'true' && steps.verify.outcome == 'success'
if: steps.published.outputs.hit != 'true' && steps.artifact.outputs.hit != 'true' && steps.payload.outputs.cache-hit != 'true' && steps.verify.outcome == 'success'
uses: actions/cache/save@v4
with:
path: ${{ inputs.runtime-root }}/third_party/v8-android
key: android-v8-payload-${{ runner.os }}-${{ runner.arch }}-${{ steps.recipe.outputs.hash }}
# The copy that outlives this ref. A pull request's own cache entry reaches that pull request
# only, so without this upload a payload built by a PR is lost the moment the PR closes, and a
# dispatch run on the head branch cannot warm the PR at all. Seven days covers the PR's review
# life; a payload this old is rebuilt once and republished, never silently reused stale.
- name: Publish the payload to every ref that shares this recipe
if: steps.published.outputs.hit != 'true' && steps.artifact.outputs.hit != 'true' && steps.payload.outputs.cache-hit != 'true' && steps.verify.outcome == 'success'
uses: actions/upload-artifact@v7
with:
name: v8-android-${{ steps.recipe.outputs.hash }}
path: ${{ inputs.runtime-root }}/third_party/v8-android
if-no-files-found: error
retention-days: 7
# The 3.2 GB source state only ever saves when the build was cut off mid-compile (timeout or
# cancel); a successful build saves the payload alone. Saving it always is what evicted every
# other repo cache entry. A cancelled pull request is superseded, not interrupted: its newer run
# starts its own build, and each such 3 GB save evicted the ccache, Gradle and Cargo entries.
# other repo cache entry. A superseded pull request saves too, because its Ninja progress is the
# only thing the next push can resume from — bounded to one entry per (os, arch, recipe) by the
# `prune-cache` job in ci-janitor.yml, which drops the older source states of the same recipe.
- name: Save resumable V8 source state
if: >-
always() && steps.published.outputs.hit != 'true' && steps.payload.outputs.cache-hit != 'true' &&
(steps.build.outcome == 'failure' ||
(steps.build.outcome == 'cancelled' && github.event_name != 'pull_request'))
always() && steps.published.outputs.hit != 'true' && steps.artifact.outputs.hit != 'true' &&
steps.payload.outputs.cache-hit != 'true' &&
(steps.build.outcome == 'failure' || steps.build.outcome == 'cancelled')
uses: actions/cache/save@v4
with:
path: |
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/ci-janitor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,52 @@ on:
# Cancelling a run is an Actions write, and this is the one workflow here that asks for one. A
# token that cannot do it reports a green job while cancelling nothing, which is the exact
# failure this workflow exists to prevent — so the grant is explicit rather than inherited.
# `contents: read` is here for the checkout the cache prune needs; it runs no job that reads more.
permissions:
actions: write
contents: read

jobs:
prune-cache:
# The repository cache is 10 GiB for every lane together, and a full board spends more than
# that: on 2026-11-03 the 56 live entries measured 10.2 GiB, and the develop warm run
# 41575174097 restored no compiler cache at all — `Cache not found for input keys:
# native-ccache-ci-test-native-Linux-X64-gcc-3ea15707…` — while that lane's own 20 MB entry had
# just been evicted. GitHub evicts by last access, so the cheapest victim is the small hot key
# every board needs. This job keeps the total under the stated budget (`scripts/ci-cache-prune`)
# and bounds the 3.2 GB resumable V8 state to one entry per recipe, so a save in one lane can no
# longer evict another lane's hot key. A hot key keeps only its newest copy per ref: an older
# copy is one another key replaced, and it is what the plan drops first (PRD-628).
if: github.event_name == 'workflow_run'
# Two janitors must not delete at once: both read the same cache list, so the second one's
# deletes land on entries the first already removed and the budget it computed is stale. A
# queued run waits for the running one instead of cancelling it.
concurrency:
group: ci-janitor-prune-cache
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Drop the entries the stated budget does not fit
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh api "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100" --paginate \
--jq '.actions_caches[]' > "$RUNNER_TEMP/caches.jsonl"
node scripts/ci-cache-prune.mjs "$RUNNER_TEMP/caches.jsonl" \
> "$RUNNER_TEMP/delete.txt" 2> "$RUNNER_TEMP/prune-summary.txt"
cat "$RUNNER_TEMP/prune-summary.txt" | tee -a "$GITHUB_STEP_SUMMARY"
deleted=0
while read -r id; do
[ -n "$id" ] || continue
# An entry that expired between the list and the delete is the normal race.
gh api -X DELETE "repos/$GITHUB_REPOSITORY/actions/caches/$id" >/dev/null 2>&1 || true
deleted=$((deleted + 1))
done < "$RUNNER_TEMP/delete.txt"
echo "deleted $deleted cache entr(ies)" | tee -a "$GITHUB_STEP_SUMMARY"

retry-flakes:
if: >-
github.event_name == 'workflow_run' &&
Expand Down
Loading
Loading