Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2c1ed5f
ci(prd-480): the tn-local runner stack, without compose
jonit-dev Oct 2, 2026
88b1606
ci(prd-480): route every movable Linux job through the TN_RUNNER switch
jonit-dev Oct 2, 2026
a9b2489
fix(ci-runners): the runner image works from /home/runner, where conf…
jonit-dev Oct 2, 2026
1221df7
fix(ci-runners): the image carries Node 20 and gh, as hosted ubuntu-2…
jonit-dev Oct 2, 2026
0630f6a
fix(ci-runners): hosted parity — Rust and media tools, and CPUs pinne…
jonit-dev Oct 2, 2026
2531ee1
fix(ci-runners): five slots on whole cores, two cores and the RAM kep…
jonit-dev Oct 2, 2026
8cc50e5
fix(ci-runners): drop mesa-vulkan-drivers from the image, as hosted h…
jonit-dev Oct 2, 2026
cd2925a
docs(prd-480): pinned five-slot pool, host reserve, and a reserved li…
jonit-dev Oct 2, 2026
c44c3e3
ci(prd-480): give small joins an unpinned tn-local-light lane
jonit-dev Oct 2, 2026
ff6d26e
fix(ci-runners): JDK 17 in the image, as hosted has it
jonit-dev Oct 2, 2026
f27b31b
fix(ci-runners): Android platform-tools at the hosted ANDROID_HOME
jonit-dev Oct 2, 2026
5780709
fix(ci-runners): slot loop reads shape and labels from the right posi…
jonit-dev Oct 2, 2026
94732de
fix(ci-runners): C.UTF-8 locale, as hosted runners have
jonit-dev Oct 2, 2026
1fc3886
ci(prd-480): route the native-platforms Linux legs onto the tn-local …
jonit-dev Oct 2, 2026
b1ae34f
docs(prd-480): AC-1 green on tn-local; focused checks then push while…
jonit-dev Oct 2, 2026
c15d2fd
fix(ci-runners): emulator lane and starter matrix pass on tn-local
jonit-dev Oct 2, 2026
11d686c
fix(ci): Windows packaging retries the Chocolatey installs
jonit-dev Oct 3, 2026
0912cf5
Merge origin/develop (PRD-481) into the PRD-480 lane
jonit-dev Oct 3, 2026
49e2f5e
ci(prd-480): Android emulator parity stays hosted
jonit-dev Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
scope:
name: Change scope
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 5
outputs:
scope: ${{ steps.classify.outputs.scope }}
Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:
typecheck:
needs: [scope, build-artifacts]
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 15
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -122,7 +122,7 @@ jobs:
# tree skips it too — every gate it would run here already passed in the run that tested this
# exact tree, and `!= 'prose'` would otherwise have bought a second full lint.
if: needs.scope.outputs.selection == 'full' || needs.scope.outputs.selection == 'instructions'
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 10
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -178,7 +178,7 @@ jobs:
test:
needs: scope
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 45
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -225,7 +225,7 @@ jobs:
test-unit:
needs: [scope, build-artifacts]
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 30
strategy:
fail-fast: false
Expand Down Expand Up @@ -263,7 +263,7 @@ jobs:
test-native:
needs: [scope, build-artifacts]
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 75
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -538,7 +538,7 @@ jobs:
test-browser:
needs: [scope, build-artifacts]
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 30
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -592,7 +592,7 @@ jobs:
test-playtest:
needs: [scope, build-artifacts]
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 30
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -633,7 +633,7 @@ jobs:
template:
- starter
- platformer
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
# The scenario arithmetic this number came from moved out to `template-nonvisual`, which now
# owns the non-visual sweep for all eight templates. What is left is `verify:golden-path` —
# pack, scaffold, install, dev, test, build, assert, once per matrix leg — which measured 309s
Expand Down Expand Up @@ -820,7 +820,7 @@ jobs:
- { template: starter, shard: "2/3" }
- { template: starter, shard: "3/3" }
- { template: tower-defense, shard: "1/1" }
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 30
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -919,7 +919,7 @@ jobs:
# as cancelled, so the skip is correct there; it still cannot be mistaken for a pass, because a
# cancelled run has no passing checks to merge on.
if: ${{ !cancelled() && needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 5
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand All @@ -938,7 +938,7 @@ jobs:
benchmark:
needs: [scope, build-artifacts]
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 25
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -992,7 +992,7 @@ jobs:
build:
needs: [scope, build-artifacts]
if: ${{ !cancelled() && needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 2
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand All @@ -1014,7 +1014,7 @@ jobs:
build-artifacts:
needs: scope
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 20
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -1058,7 +1058,7 @@ jobs:
performance-contracts:
needs: scope
if: ${{ needs.scope.outputs.selection == 'full' && !cancelled() }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 2
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -1124,7 +1124,7 @@ jobs:
budgets:
needs: [scope, build-artifacts]
if: ${{ needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 15
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -1250,7 +1250,7 @@ jobs:
ci-required:
needs: [scope, native-platforms, typecheck, lint, test, test-unit, test-native, test-browser, test-playtest, golden-path-template, template-nonvisual, golden-path, benchmark, build, build-artifacts, budgets, performance-contracts, supply-chain]
if: ${{ always() && !github.event.pull_request.draft }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 5
# A reused verdict is only a pass if the run it cites passed its own ci-required job, and that
# is an Actions API read rather than anything this repository can compute.
Expand Down Expand Up @@ -1310,7 +1310,7 @@ jobs:
# `!cancelled()` and not `always()`, for the reason golden-path gives above: a cancelled run
# has nothing to report and its summary would only describe the cancellation.
if: ${{ !cancelled() && needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 5
permissions:
contents: read
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
paths:
name: lanes this pull request touches
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 5
permissions:
contents: read
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
name: csg
if: ${{ needs.paths.outputs.csg == 'true' && !github.event.pull_request.draft }}
needs: paths
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 10
defaults:
run:
Expand All @@ -87,7 +87,7 @@ jobs:
name: ik
if: ${{ needs.paths.outputs.ik == 'true' && !github.event.pull_request.draft }}
needs: paths
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 10
defaults:
run:
Expand All @@ -106,7 +106,7 @@ jobs:
name: vegetation
if: ${{ needs.paths.outputs.vegetation == 'true' && !github.event.pull_request.draft }}
needs: paths
runs-on: ubuntu-latest
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 10
defaults:
run:
Expand All @@ -125,7 +125,7 @@ jobs:
name: decals
if: ${{ needs.paths.outputs.decals == 'true' && !github.event.pull_request.draft }}
needs: paths
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
Expand Down Expand Up @@ -217,7 +217,7 @@ jobs:
name: exposure
if: ${{ needs.paths.outputs.tone == 'true' && !github.event.pull_request.draft }}
needs: paths
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
Expand Down
42 changes: 30 additions & 12 deletions .github/workflows/native-platforms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ jobs:
# The caller is the single selection authority. Dispatch has no exemption inputs and is full.
scope:
name: Validate caller selection and candidate
runs-on: ubuntu-latest
# PRD-480. `github.event` inside a `workflow_call` is the caller's event payload, so this
# expression reads the same `pull_request` fork flag ci.yml did when it called this file: a fork
# PR falls through to hosted runners and never reaches the owner's machine.
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 5
outputs:
scope: ${{ steps.classify.outputs.scope }}
Expand Down Expand Up @@ -89,7 +92,7 @@ jobs:
if: >-
needs.scope.outputs.selection == 'full' &&
inputs.ios_only == false
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 30
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -136,7 +139,7 @@ jobs:
name: Android V8 source payload
needs: scope
if: needs.scope.outputs.selection == 'full' && inputs.ios_only != true
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
# 180-minute build ceiling plus 30 minutes for actions/cache/save, per the action's note.
timeout-minutes: 210
outputs:
Expand Down Expand Up @@ -186,6 +189,9 @@ jobs:
needs.android-v8-source.outputs.published == 'false' &&
github.event_name != 'pull_request' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
# PRD-480 leaves this one on GitHub's machines: it is the release write itself — `contents:
# write` and a `gh release upload` below — and no pool container is handed a token that can
# publish. It is named on the hosted allow-list in ci-structure.spec.ts.
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
Expand Down Expand Up @@ -213,7 +219,7 @@ jobs:
name: Native collector evidence coverage
needs: [scope, android-emulator-parity, desktop-parity, desktop, ios-simulator]
if: ${{ !cancelled() && needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 5
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -248,7 +254,7 @@ jobs:
name: Networking qualification matrix
needs: [scope, android-emulator-parity, desktop-parity, desktop, ios-simulator]
if: ${{ always() && needs.scope.outputs.selection == 'full' }}
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER_LIGHT) && 'ubuntu-24.04' || vars.TN_RUNNER_LIGHT }}
timeout-minutes: 5
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -316,6 +322,8 @@ jobs:
needs.scope.outputs.selection == 'full' &&
inputs.ios_only != true &&
needs.android-v8-source.result == 'success'
# Hosted, measured: SwiftShader renders the emulator's GPU on the CPU, and on a 4-thread tn-local
# slot run 37082733117 spent 41 min of this job's 45 still running APKs (hosted finishes in ~30).
runs-on: ubuntu-24.04
timeout-minutes: 45
env:
Expand Down Expand Up @@ -447,8 +455,14 @@ jobs:
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# `|| true` on both udev calls, because the routed `tn-local` container cannot do what
# they do: no udev is running in it and its /sys is read-only, so `udevadm trigger`
# fails there for a reason that has nothing to do with acceleration. The container does
# not need them — `scripts/ci-runners.sh` passes `--device /dev/kvm` and the image's
# entrypoint sets its mode before the job starts — and a step that fails on the way to
# the report below costs this lane the whole emulator run.
sudo udevadm control --reload-rules || true
sudo udevadm trigger --name-match=kvm || true
# Report the mode, never assert it. `test -w /dev/kvm` as the last line of this step —
# copied from native-release.yml, where nothing has exercised it — turned "this runner
# has no KVM" into a failed job, which is worse than the slow boot it was meant to fix.
Expand Down Expand Up @@ -740,7 +754,7 @@ jobs:
if: >-
needs.scope.outputs.selection != 'prose' &&
inputs.ios_only == false
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 20
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -812,7 +826,7 @@ jobs:
if: >-
needs.scope.outputs.selection == 'full' &&
inputs.ios_only != true
runs-on: ubuntu-24.04
runs-on: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
timeout-minutes: 75
env:
TN_CI_SHA: ${{ needs.scope.outputs.candidate_sha }}
Expand Down Expand Up @@ -1242,8 +1256,12 @@ jobs:
# The release container is a ZIP on macOS and Windows and desktop-distribution.mjs shells
# out to `zip -r -q`. A missing archiver must be a red job, never a silent pass.
if [ "${{ matrix.platform }}" = "Windows" ]; then
choco install zip -y --no-progress
choco install nsis --version=3.11.0 -y --no-progress
# The community feed drops requests now and then ("package was not found" for a listed
# version, run 37079046851). Retry twice; makensis -VERSION below still fails closed.
for attempt in 1 2 3; do
choco install zip -y --no-progress && choco install nsis --version=3.11.0 -y --no-progress && break
[ "$attempt" = 3 ] || sleep 30
done
nsis='/c/Program Files (x86)/NSIS'
"$nsis/makensis.exe" -VERSION
cygpath -w "$nsis" >> "$GITHUB_PATH"
Expand Down Expand Up @@ -1766,7 +1784,7 @@ jobs:
# the one WebGPU backend that publishes an arm64 Linux archive. scripts/download-deps.mjs
# and scripts/native-build.mjs select that route from `process.arch`.
- platform: linux-x64
runner: ubuntu-24.04
runner: ${{ (github.event.pull_request.head.repo.fork || !vars.TN_RUNNER) && 'ubuntu-24.04' || vars.TN_RUNNER }}
- platform: linux-arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ TypeScript 5.9 `strict`, **ESM only**; relative imports end in `.js`. Versions c
Results go in the existing PRD, PR or response; a separate evidence file only when the user asks or a workflow consumes it. Prose edits need only the doc checks and mirror regeneration — no implementation checkpoints or artificial negative controls.

**Fail closed**: malformed input throws, a missing observation fails, an empty assertion set fails. `pnpm test` proves units; **a playtest scenario proves the game** — every runtime-behaviour change gets one, rerun on each later change to that behaviour.
**Nearest lane first, CI last.** A CI round trip costs over an hour; prove it locally (unit test, playtest, emulator, attached device) and push once green. **Prefer an emulator over hardware** when it can hold the claim; a stopped emulator means start it, not "blocked". Never call `xvfb-run` (its exit status is its own cleanup kill; `sh scripts/xvfb.sh <cmd>` is the wrapper). An unnamed WebGPU adapter may be SwiftShader: pass `--browser-recipe webgpu` and check `adapter.info`. Free a port with `lsof -ti tcp:<port> | xargs -r kill`; `pkill -f vite` kills your own shell.
**Nearest lane first, CI last.** A CI round trip costs over an hour; prove it locally (unit test, playtest, emulator, attached device) and push once green. While `TN_RUNNER` is set, run only the focused checks for what you changed and push: the full board runs once, on `tn-local`. **Prefer an emulator over hardware** when it can hold the claim; a stopped emulator means start it, not "blocked". Never call `xvfb-run` (its exit status is its own cleanup kill; `sh scripts/xvfb.sh <cmd>` is the wrapper). An unnamed WebGPU adapter may be SwiftShader: pass `--browser-recipe webgpu` and check `adapter.info`. Free a port with `lsof -ti tcp:<port> | xargs -r kill`; `pkill -f vite` kills your own shell.
Long gates write `artifacts/gates/status.json`: read it with `pnpm gate:status`, probe a stale phase with `pnpm gate:doctor`; `pnpm gate:resume` continues only while worktree, branch, HEAD, lease and artifact still match. None of them repair or remove a worktree.

## Working outside the repo
Expand Down
Loading
Loading