Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).
Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.
uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
extract-plugins.py derives addedAt/updatedAt per entry from one `git log
--name-status` over plugin-catalog/ (committer dates, so rebase-merged PRs
are stamped when they land; renames carry the original addedAt). A shallow
checkout or missing git yields null dates instead of wrong ones, and
deploy-site.yml now checks out with fetch-depth: 0 so the deploy has the
history.
The /docs/plugins page gains a Sort control (Most starred / Newest /
Recently updated) and an "Added … · Updated …" line on every card; Updated
is hidden while it equals Added.
`generate-skill-docs.py` now deletes every page under `user-guide/skills/{bundled,optional}`
it did not write this run, together with the zh-Hans mirror twin, so a skill that moves,
merges or leaves the shipped set takes its page with it instead of lingering as an orphan
that cross-links still reach (24 such pages after the shipped-set slim, plus 21 zh-Hans
copies whose English page was already gone).
The Docs Site Checks workflow regenerated the docs and never compared the result with the
committed copies, which is what GitHub renders; it now fails with a pointer to the generator
when they differ. Also regenerates the two pages that drifted since the salvaged commits.
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim
salvage + its test and workflow steps dropped), the shim re-exec stays retired,
package.json carries no electron-builder block (afterExtract identity stamp wired
into electron-builder.config.cjs instead; after-pack.mjs keeps signing only),
Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap
(export_scratch_tmp_env) is taken and re-run after profile resolution.
Conflicts resolved toward the branch: PM owns dependency preparation, the
Windows shim re-exec/hand-off path stays retired (main's shim-parent wait,
gateway-resume env token and update_cmd_deps tests dropped), docs describe
the PM update flow. The docker workflow parks install-stamp.json around the
toolchain step instead of deleting it so tests/docker can compare provenance.
scripts/check_no_tmp_literals.py flags /tmp path tokens in production code, skills,
docs and prompt strings (tests, CI workflows, Dockerfiles, lockfiles, i18n mirror,
code comments and docstrings exempt; ${TMPDIR:-/tmp} idiom exempt). Opt out one line
with 'no-tmp: ok — <why>' on the line or the line above. _BASELINE lists pre-existing
hits per file: growth fails, burn-down is advisory (--strict-baseline / --print-baseline
to refresh). Wired into lint.yml next to check_compat_pointers.
Install-Uv accepted any file at $HermesHome\bin\uv.exe, and copied whatever
`Get-Command uv` returned into that location. Chocolatey's bin\uv.exe is a
ShimGen launcher that locates ..\lib\uv\tools\uv.exe RELATIVE to itself, so
the copy is dead on arrival; `& exe --version` does not throw on a nonzero
exit, so the launcher passed the try/catch and the Python stage then failed
with "Python 3.11 not available" (#110350). The re-run path trusted the same
broken copy again.
Building on KoNit-K's Test-ManagedUvBinary and its three call sites:
- Test-ManagedUvBinary merges stderr, relaxes the error preference, and
returns the `uv <version>` line only on exit 0 -- a launcher's error text
can no longer surface as "Managed uv found (Cannot find file ...)".
- Resolve-UvShimTarget maps a candidate to the standalone binary before the
copy: `<name>.shim` sidecar (Scoop), the Chocolatey bin\ -> lib\<pkg>\tools\
layout, symlinks (winget Links\); other reparse points (WindowsApps
app-execution aliases) have no copyable file and skip the salvage.
- The salvage rung validates the candidate where it lives, copies, then
validates the COPY at its new location and removes it on failure, so the
stage fails honestly instead of reporting success over a dead launcher.
- scripts/tests/test-install-ps1-uv-shim-validation.ps1 drives the real
Install-Uv with compiled fake uv binaries (a working uv and a
location-relative launcher) under stubbed installer rungs; wired into
installer-tests.yml for pwsh 7 and Windows PowerShell 5.1.
Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
Every unflagged `hermes update` resolves its channel through R2 now, so the
mocked update harnesses reached the network (and 404 today). An autouse
fixture in tests/hermes_cli defaults every channel to a source-branch record
through the documented `_resolve_channel` seam; archive/transport tests keep
the real reader via the `real_release_channels` marker. The stable-identity
test pins the channel's commit through that seam instead of the retired
release-candidates pointer, and the completion-process fixture ships
source_completion.py in its NEW tree.
Desktop: js-tests installs the dev extra (the electron contracts drive real
hermes_cli/pm code through HERMES_PYTHON); the source-backend fixture runs PM's
worker on PM's staged runtime; async canImportHermesCli is awaited; the
feature-flag test pins the platform rule; a mocked profile store exports what
settings-scope reads; the peer-device e2e imports the mock-provider helpers
from tests-js like its siblings.
write_install_stamp.py marks the checkout distribution=docker for the image
build; left in place, PM on the runner looks for the image's packaged runtime
and fails with "packaged PM runtime is missing".
Root-level tests/ is for root-level modules; 19 files testing scripts/, 3 testing
pm/ and 5 testing hermes_cli/ move to the mirrored directory (workflow file lists
and cross-imports updated; path arithmetic bumped one level).
tests/pm gains a conftest with the isolated_machine_home fixture that seven
modules had copy-pasted verbatim.
Four origin/main merges brought back `linux_only` / `macos_only` /
`windows_only` marks in 41 test files, along with the pre-platforms()
versions of scripts/ci/list_os_marked_tests.py and check_os_marker_fakes.py.
Because the legacy names are no longer registered, pytest treated them as
unknown marks — a warning — so every Windows- or macOS-only test RAN on
Linux (test_local_runtime_recovery.py tripped the live-system kill guard).
Rewrite the marks, restore the platforms()-aware CI scripts (keeping main's
os.walk fix for vanishing __pycache__ dirs), drop the stale _BASELINE entries,
and make the conftest reject the retired marks outright so the next merge
cannot resurrect them silently.
Docs under website/docs were linked with Docusaurus site routes
(`](/getting-started/installation)`, `](/docs/user-guide/x)`), which GitHub's
file viewer resolves as repository paths and 404s (#114428). Relative Markdown
file links (`](../user-guide/x.md#anchor)`) are followed by both GitHub and
Docusaurus, so that becomes the authoring convention:
- website/scripts/check_doc_links.py lints hand-authored EN + zh-Hans pages
for route-style links (`--fix` rewrites them, refusing any route that maps
to no doc file); wired into the Docs Site Checks workflow and
tests/website/test_check_doc_links.py.
- generate-skill-docs.py emits the same relative form for related-skill and
catalog links instead of `/docs/user-guide/skills/...`.
- src/remark/relativeDocLinks.js rewrites `./x.md`/`../x.md` to
content-root-absolute `/x.md` before Docusaurus resolves links, so a
relative link still resolves in the zh-Hans build when source and target
sit on different sides of the translation fallback (Docusaurus resolves
`./`/`../` only against the source file's own directory).
- website/README.md states the convention and points at the checker.
Trim the salvaged retry to the case the logs show: `_get_json` returning None
(timeout / non-200) is a hole in the cursor walk, not the end of the catalog.
A dict page with an empty item list stays terminal as before, and the
interactive browse path (12 s budget) no longer sleeps up to 8 s per retry.
Constant `CATALOG_PAGE_RETRIES` replaces the local literal.
Workflow: `skills-index.yml` `timeout-minutes` 50 -> 120. Since Sep 17 ClawHub
serves ~10 s per 200-item page (was ~2.5 s), so the ~400-page walk alone takes
60-70 min; the 06:28 UTC runs on Sep 17 and Sep 18 were cancelled at 50 min with
the clawhub crawl still running, and the 18:19 run stopped at 12,145 skills
(< 20,000 floor) after one failed page. Both halves are needed for the index to
ship again.
require_pushed() proves ancestry against origin's advertised refs from
the local clone. The publish job's depth-1 checkout cannot do that once
main advances past the built SHA, failing every later dispatch.
The step globbed uv-* under $HERMES_RUNTIME_DIR\tools, but that variable already
points at the tools directory (D:\a\_temp\setup-pm\tools), so it looked for
...\tools\tools and died with "Cannot find path". Verified against a real
toolchain layout with HERMES_RUNTIME_DIR set to the tools directory, the way CI
sets it.
uv is not on PATH for workflow steps ("The term uv is not recognized"), so the
pywinpty install for pty-run.py resolves uv.exe out of $HERMES_RUNTIME_DIR\tools
-- the same place setup-pm puts the toolchain -- and still goes through
uv pip install.
The toolchain python ships without pip (`No module named pip`), so the
pywinpty install for pty-run.py goes through uv -- the installer the
toolchain itself is built with -- and the step proves the import landed
instead of trusting the install to have worked.
The windows user-state step ran the installed CLI's one-shot chat with its
stdout piped into the evidence log:
& $hermes chat -q "..." 2>&1 | Add-TsPrefix | Out-File $log
A released tag prints through prompt_toolkit, whose create_output() on Windows
returns Win32Output, and that constructor needs a console screen buffer. A pipe
has none, so every windows leg that reached this step died with
prompt_toolkit.output.win32.NoConsoleScreenBufferError:
No Windows console found. Are you running cmd.exe?
HEAD guards its own print path (cli.py's _pt_print_ansi falls back to print),
but the tags under test cannot be changed, so the console has to come from the
rig. tests/install/e2e-assets/pty-run.py runs the command under a pseudoconsole
and captures what the child writes; the driver uses it for that turn, and the
windows job installs pywinpty (the same ConPTY spawner the product uses on
Windows, pinned to the version pyproject requires) for the runner. Without
pywinpty the runner falls back to a pipe and says so in the log, so a missing
dependency degrades the evidence rather than silently changing what the leg
proves.
Verified against a real installed venv on Windows, not just in CI:
- piped stdout reproduces the CI traceback exactly (defaults.py:91 ->
Win32Output -> win32.py:219 raise NoConsoleScreenBufferError);
- under pty-run.py the same print path succeeds for the old CLI's exact call
(print_formatted_text(ANSI(...)) through _cprint), and the capture is clean.
Catalog CI validates each pinned tree in a venv that has only hermes-agent, so any plugin whose
code arrives through pyproject dependencies (the wrapper shape from #113851) failed the probe with
"No module named ...". The flag runs the same constrained install `plugins install` would, then
probes; the workflow passes it. Live: mnemosyne pin fails without the flag, passes with it.
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
The self-lock/holder preflight (#99711) deferred the repair on the theory
that the updater's own mapped python.exe makes the venv rename impossible.
Live on windows-latest, a process executing from venv\Scripts\python.exe
(and the repo's real .venv with cp311 .pyd extensions loaded) does NOT block
the rename; what blocks it with WinError 5 is any ordinary handle under the
tree: a process cwd, an open file, a sync client. Since sys.executable is
always under the venv on Windows, the deferral fired on every run and no
Windows install could repair from `hermes update`. Retire it and its tests;
the pyvenv.cfg repoint needs no rename and has none of that exposure. The
wine2e lane now runs the cutover probes instead.
The repoint keeps the live venv's site-packages, so provisioning's
fall-forward to the next minor (3.11 -> 3.12, #76106) must not be pointed at
a cp311 tree: refuse before touching pyvenv.cfg. The live Windows test holds
the venv the way the field does (a child with cwd inside it), asserts the
rename path fails (the symptom) and the repoint succeeds; the rollback and
minor-guard tests run on every host.
A channel dispatch sends both `channel` and `build_commit` (the commit is
provenance, not a mode), so the tag/commit staging steps — gated on
`inputs.build_commit != ''` — ran inside a channel build. There a pinned
request forces HERMES_PAYLOAD_TAG and HERMES_BUILD_COMMIT empty, so the step
fell through to tag mode and handoff refused the empty tag:
ValueError: Invalid release handoff identity (scripts/releases/handoff.py:24)
Channel packages are already staged by the pinned-request step, so gate both
per-platform staging steps on the channel signal the rest of the workflow
uses. The commit-only status page had the same flaw one job later: it would
fetch commit-namespace receipts a channel build never wrote and publish a
false "not built" matrix under releases/commit/<sha>/. The channel flow
renders its own page in publish-channel.
Dropping archive-inputs from the result jobs replaced the whole `needs`
list with the job's own name: `build-win32` needed `build-win32`, and
`build-darwin` needed `build-darwin`. GitHub rejects a workflow with a
cyclic job dependency outright, so the dispatch died at parse time — and
the result jobs read `needs.build-win32-release.result` /
`needs.build-win32-commit.result` (the darwin pair likewise), which are
only reachable through a declared `needs` entry.
Name the trust-branch legs instead, and pin the contract: no job may
need itself, and every `needs.<job>` read in an `if:` or expression must
be a declared dependency.
The archive-inputs reusable-workflow job existed only to serialize one
command behind validate. Run scripts.ci.archive_inputs directly inside
validate with the job's own gate shape (skipped for disposable and
termux-only runs), so the archiving never gates or skips the native
build legs and the whole dispatch loses one job-level hop.
- validate job now always does a full checkout (fetch-depth 0 + tags) so
require_pushed can see the branch that actually carries build_commit
- HERMES_BUILD_COMMIT is only set for pure commit builds, not channel or
disposable builds (which also set build_commit but carry their identity
in the channel request)
scripts/ci/check_os_marker_fakes.py flags test files that make the
interpreter believe it is on macOS (is_macos -> True, sys.platform ->
"darwin", platform.system -> "Darwin") while carrying no `macos_only`
marker: the macOS lane imports only marked files, so such a file is green
on Linux over a faked branch and never runs on the host it exists for.
A `# os-marker: ok — <why>` comment opts a host-independent line out.
_BASELINE holds the files that already faked macOS when the check landed;
a stale entry fails the check so the list can only burn down. Wired into
lint.yml next to the compat-pointer check; two invariant tests cover a
flagged fake vs marked/opted-out files and host-honest platform reads.
The zsh login-shell legs in remote-lifecycle.test.ts and
ssh-connection.test.ts silently returned when zsh was missing, and the
js-tests runner image ships no zsh, so the #111949 coverage never ran on
CI and a wrapper regression stayed green.
- js-tests.yml: install zsh on the Linux runner before the checks.
- Both legs now report vitest skips ('zsh not installed') instead of
passing; the ssh-connection leg is its own test so the skip is visible.
- Docs: note the zsh degraded mode (no process-group kill for a hung
probe's grandchildren) in the SSH connection guide.
The local --channel command no longer touches R2: it resolves the exact
pushed commit and dispatches the default-branch workflow, whose privileged
allocation step creates the channel and mints the immutable build request.
The disposable allocation path is generalized to cover the unscoped
production preview, gated by a new `channel` workflow input; build legs
consume the same channel-build / channel-request-sha256 job outputs as
before. The anti-tamper gate is now commit_build.admit (maintainer
permission) running inside the allocate step.
Drop the resume path: --resume-channel-build / --request-sha256 and
resume_build() are gone, and allocate_protected no longer recovers a lost
request PUT by sequence. Retrying re-dispatches and mints a fresh sequence
slot; idempotency survives via the deterministic build ID and the existing
immutable-request dedup. Keep the preview allocate `lastAllocation` build-ID
field (concurrent-CAS uniqueness), which is not resume.
channel_public_base now defaults to the documented production origin like
the commit-build path, so a local command names its page without a
hand-set CLOUDFLARE_R2_PUBLIC_URL.
Also drops a stale fork-isolation assertion left behind by the
fork-conditional dispatch removal.
The scan is detection-only and its findings are the repo-wide baseline of
CVEs in pinned dependencies — identical for every PR, unrelated to any
PR's diff. Reporting that baseline in each PR's review comment read as
"this PR has 76 vulnerabilities" to contributors, and the SARIF upload
tripped GitHub's per-installation API rate limit during merge trains.
The scheduled weekly run (plus workflow_dispatch) keeps feeding the
Security tab; the per-PR workflow_call, the review_status wrapper job,
and the orchestrator's now-unneeded SARIF permissions are removed.
scripts/check_profile_scope_patterns.py runs the validated hazard regexes in
scripts/ci/profile_scope_patterns.json (18 of the 31 campaign patterns: every one has a scope_hint
and hits <= 50 sites on main; the wider ones are review greps, not lint) against the lines added
vs the PR base and prints file:line, pattern id/class and why. Always exits 0: most shapes have
legitimate sites (a standalone `hermes -p x` process where environ IS the profile), so the
reviewer reads each finding against its scope hint. Wired into lint.yml beside the public-surface
diff with continue-on-error.
Proof: the pre-fix tools/bot_relay.py (`env = dict(os.environ)`, before the served_profile_child_env
change) is flagged as P05/C2; the fixed file and this branch's diff vs main report 0 findings.
Test: a fixture with the hazard is flagged on the right lines, the scoped-builder version is not,
and the line filter hides hits outside the added range.
The CI cache barely ever restored anything useful, for four independent
reasons found in live run logs and the fork's cache store:
- `uv cache prune --ci` (setup-pm/prune) discards downloaded wheels before
the save, so the snapshot carried only source-built wheels — the next
run "restored" it and still cold-downloaded everything. Upstream main's
logs showed the end state: a 209KB stub cache exact-hitting forever.
- Tool-only jobs (icons-freshness etc.) auto-saved a 1.5KB empty uv cache
under the production exact key; caches are immutable, so the stub won
forever and blocked real saves.
- The npm cache key had no restore-keys, so one lockfile bump missed the
exact key and every npm job went cold.
- `2efa4ff94f` deleted the electron-builder toolchain save but left the
assemble job restoring `eb2-` — a fossil nobody regenerates; assembly
cold-downloads winCodeSign/ATS/dotnet on every release.
Changes:
- pm/cache_lock.py: move prune_uv_cache_to_lock out of
scripts/bundles/native.py (which re-exports it); the lock-exactness
contract now serves both the bundle ship gate and CI caches.
- pm.build_env learns --exact-lock --lock-source: prune cache entries the
project uv.lock cannot resolve, keeping lock-required downloaded wheels
(unlike --ci). Refuses --ci/--prune-cache combinations.
- setup-pm: python-cache auto-save now requires extras (no stubs from
tool-only jobs); key drops the prune flag and bumps to v3 — pruned and
unpruned saves share one namespace since both are lock-exact; pre-save
pruning switched from --ci to --exact-lock; npm cache gains a
lockfile-agnostic restore prefix.
- save-pm-cache: same exact-lock prune before explicit saves.
- desktop-bundled-release: build legs (cache-mode: write) restore+save the
electron-builder toolchain under eb3- keyed on the locked builder
version; assemble restores the same namespace; the dead default-cache
resolution step is removed (assembly resolves no electron artifacts).
- cleanup_pm_toolchain_caches.py: match v2 and v3 smoke keys.
Validation: tests/scripts/test_bundle_native.py 11/11 (incl. both
lock-prune gates), tests/pm failures identical before/after the diff,
tests/scripts/test_bundle_payload.py 5/5, tests/ci cleanup 3/3,
tests-js setup-pm-post 1/1 and the three setup-pm-cache contract tests
updated and green (6 failures in that file pre-date this diff and are
drift between the workflow and its stale assertions); pm.build_env
--exact-lock E2E against a real uv cache copy pruned 3 stale entries and
kept the rest.
Aligns the star ranking with the rule the skills index already follows:
GitHub is consulted only by the twice-daily skills-index.yml schedule, whose
artifact every docs deploy reuses. deploy-site.yml now runs
fetch-plugin-stars.py without --probe (reuse-only: artifact → live site copy →
disk → empty) and cannot call the API at all, so a same-day merge train adds
zero requests regardless of cache age.
The probe itself collapses from one REST call per repo (~26 today, growing
with the catalog) to a single GraphQL query with aliased repository fields,
so the scheduled run costs one request no matter how big the catalog gets. A
failed probe (rate limit, renamed repo, bad token) keeps the previous counts.
Greptile's two findings on the original PR were both right.
1. The scaler read test_durations.json from the checkout, but CI ran on
a fresh runner where that file never exists (it is gitignored and the
slicing-era artifact/merge job that produced it is gone). The feature
was inert exactly where the false FLAKY kills happen. tests.yml now
restores the most recent main-saved cache before the run (PRs read
only) and saves it after a green push to main, mirroring the
ci-timings-baseline restore/save pattern already in ci.yaml.
2. _save_durations persisted every file's total subprocess wall,
including the ~cap of a timed-out attempt and the retry-summed wall
of a FLAKY file. With the scaler that compounds: a hang cached at
~300s earns 900s next run, then ~900s cached earns 2700s, until the
job timeout is the only bound. _clean_pass_durations drops failed and
FLAKY files from the write so a file's cached duration is always a
first-attempt-clean measurement; those files keep their previous
known-good entry.
Tests trimmed to the salvage bar (<=2 invariants for the scaler plus one
for the cache filter) and moved next to the other runner tests under
tests/scripts/.
Catalog entries sort official → stars desc → name, both in browse shelves and
filtered grids, with a ★ pill on each card linking to the repo's stargazers.
Rate-limit discipline is the design constraint: the docs site deploys many
times a day and shares one GitHub App API budget with every other workflow
(tonight's merge train got rate-limited on unrelated uploads). So
website/scripts/fetch-plugin-stars.py first fetches the live site's own
plugin-stars.json (a CDN GET, not the API); if that cache is under 24h old it
is reused verbatim and GitHub is never called. Only a stale cache triggers one
GET /repos/{owner}/{repo} per unique catalog repo, and a 403/429 mid-run keeps
the previous counts instead of zeroing them. extract-plugins.py merges the
cache into plugins.json (`stars`) and plugins-meta.json (`starsFetchedAt`), and
the page footnote says when the ranking was last refreshed.
osv-scanner.yml documents itself as detection-only (fail-on-vuln: false,
findings land in the Security tab) yet all-checks-pass listed it in needs,
so any failure result blocked the merge. In practice the failures are not
vulnerabilities: the "Upload to code-scanning" step hits GitHub's
per-installation API rate limit whenever several PRs run at once, and a
merge train of catalog entries went red on it across the board. The scan
still runs on every PR and weekly on main; it just reports instead of gating.
allocate-disposable and validate are both ubuntu + release-signing with
identical secrets; as separate jobs they paid a second runner warmup and
a second checkout for a step that shares the first's tree. The allocate
steps now run inside validate (guarded by disposable_channel), exposing
the same outputs via steps.allocate; validate keeps its name and output
surface so all 99 downstream needs.validate.outputs.* references are
untouched. A failed allocation fails validate and skips the run, which
is exactly what the old needs-gate produced.
Every job in the release workflow consumes exactly one commit. Tag-build
paths keep full history + tags (merge-base ancestry gate, canary MSIX
tag arithmetic); channel/commit/disposable paths fetch depth 1 with no
tags: admission exits before any history read, versions derive from the
request/env, and the two disposable-only jobs (allocate, receiver
publish) never read history at all — unconditional depth 1.
Teknium's ruling: catalog plugins do not need the 2-week maturity window,
but they may not ship an in-app updater that downloads and replaces their
own files, because that makes the reviewed SHA pin decorative. Rule 3 in
the README and item 5 on the docs page now say so, and plugin-catalog-ci
fails an entry whose catalog build both fetches from GitHub releases/raw
and writes or renames plugin files (either half alone is allowed).
A fork commit build is now ONE workflow dispatch whose run both allocates
the disposable channel and builds it. Previously allocation printed a
follow-up command that had to be dispatched separately (the GITHUB_TOKEN
recursion wall forced two runs; release.py grew poll/extract machinery to
automate the hop — all deleted now, net -144 lines).
- Lease is the run id alone (no attempt suffix): re-run failed jobs
re-enters the same namespace; succeeded allocate job is skipped and its
outputs persist. r2_scope accepts legacy <id>-<attempt> leases on read.
- allocate-disposable emits job outputs (channel_build, request digest,
lease, public base); validate consumes them in disposable mode and
re-exports a normalized pin every downstream job reads.
- Admission guards unchanged in semantics: forks still cannot run without
a disposable allocation; disposable runs still never touch production
feeds, termux, or the commit-builds page.
- Upstream trusted-controller path (channel_build inputs) byte-identical.
- Fixtures updated for run-id leases; two dead two-dispatch tests and the
fixture's allocation-probe plumbing removed; smoke matrix test skips
banana's new admission job (no toolchain by design).