The replay picked PRs with sort:updated-desc, so the sample changed whenever an old PR got a
comment and two runs could not be compared (unsupportedpastels M6). It now takes every PR merged
inside a closed date window in PR-number order, writes manifest.json (number, base, head), and
--manifest replays exactly those ranges. The summary prints one total: PRs with at least one
blocking finding, instead of mixing blocking and warning-only rows.
- `ENFORCEMENT` in scripts/code_health/config.py (blocking / advisory / off)
turns the ratchet down or off with a one-line commit to main. It is read
from the BASE revision, so it applies to fork PRs (repo variables are not
passed to fork-triggered workflows), CI and local hooks alike, and a PR
cannot relax its own check.
- `scripts/check --install-hook pre-push` judges each pushed commit the way
CI judges the PR (base = that commit's merge-base); `--commit REV` is the
underlying mode, `--uninstall-hook` removes our hooks and leaves foreign
ones. Pre-commit and pre-push share one tree snapshot that never touches
the user's worktree or index.
- A failing report names the local commands; CONTRIBUTING, AGENTS.md and the
PR template point at `python scripts/check`.
SIGKILL bounds a sync Popen.wait() that directly follows kill(), so it no
longer needs a timeout. communicate() after kill() and asyncio's
Process.wait() after kill() stay flagged: both read/close the pipes, and a
grandchild holding them blocks forever (measured: > 20 s with a background
`sleep`; the sync wait() returned in 0.00 s). The HX006 fix text now names
the reap pattern.
Fourth review round (F11-F18, N1-N4, kvnloo, Adolanium):
- Hits are old only where the base occurrence survives: a line the comment- and
whitespace-blind line diff keeps (blame semantics), or anything inside a unit whose
body is unchanged. An identical violation moved past unrelated code is new; removing
an allow comment or re-indenting is not. Hit fingerprints exclude comments (N4).
- Inline allows cover their own line; only a comment-only line covers the next (N3).
- Rules read a copy of the AST with import aliases resolved, so `sp.run`,
`from subprocess import run as execute`, aliased threading/asyncio/load_config are
seen (F12); deadline helpers must resolve to the real asyncio API, and
communicate()/wait() are judged only on receivers holding a spawned process (F13);
infinite deadlines are unbounded (F14).
- HERMES_* writes introduce a name too (N1); bool(os.environ[...]) is HX010 (N2);
HX009 needs the isinstance subject to come from gather(return_exceptions=True).
- Recursive self-references are normalised out of body hashes, so a recursive rename
keeps its cap, py and ts (F15). Decorators belong to their function's scope and
classes are matchable units, so their hits follow proven moves (F16).
- An over-cap file may not grow (F17, Teknium: strict improvement-only).
- A file the checker cannot parse or measure (ast/ruff/TS parse error) fails.
- scripts/check: --staged always lints an index snapshot (ignored files are invisible
to git status, not to guards) (F18); create the --prefix dir for Git for Windows
(F11); --only builds just the selected jobs, so it needs no ruff for non-ruff checks.
- CODEOWNERS covers scripts/check + scripts/code_health; frontend-only PRs skip the
Python-only lint jobs again and run only the ratchet.
Final review on #132646 (R1-R4), each reproduced on d03b089 first:
- Git blobs decode as utf-8-sig like working-tree reads, so a BOM no
longer makes one side unparseable: a new BOM file's debt is measured
(the hook used to let it through) and existing BOM debt is not new (R1).
- One walker, _eager(), yields what evaluating a node runs now: a
lambda/def contributes only its defaults and decorators, a generator
only its first iterable. HX005, HX006 and HX007 all use it, so a
coroutine defined inside `asyncio.timeout()` is not treated as bounded
and a returned sync loader is not a sync call in the coroutine (R2).
- `wait_for(fut=..., timeout=...)` is recognized like the positional
form (R3).
- HX005 also checks lambda defaults, `if`/`while` tests, `with` context
expressions, match guards and walrus bindings; a generator's body stays
deferred (R4).
Second-round review on #132646 (F1-F10), each reproduced on 2531fb2 first:
- Moves are matched one-to-one: unchanged units are reserved first, then
same-file body, then cross-file body only when the origin's name is gone,
then same name. A copy of unchanged debt is new debt; a function moved
over a same-name stub keeps its own cap (F1, F3).
- Every base hit is owned by exactly one head scope, so a file rename no
longer counts each old hit twice (F2).
- Ruff runs when either side has Python, so a delete-only .py diff with a
TS edit no longer crashes on a missing ruff result (F4).
- Deadlines must be finite: timeout=None, **{} and asyncio.timeout(None)
are no deadline; awaited calls inside `async with asyncio.timeout(x)`
are bounded (F5, F8).
- HX005 walks only what runs at import: the import-time branch of a
`__name__` guard (either operator), loops, match, try*, decorators,
class bases; a lambda value is deferred (F9).
- `health: allow` counts only in real comment tokens (Python tokenize, TS
comment trivia), never in string literals (F7).
- TS function lines are owned by the innermost function, so siblings on
one line give it up once (F10).
- The pre-commit hook runs `scripts/check --staged`: health compares the
index tree, and when the working tree differs from the index the other
checkers run on a checkout-index copy with a copied index, so commits are
judged on the staged bytes and unstaged work is never touched (F6).
Independent review of #132646 found five defects, each reproduced and fixed:
- Pattern hits from any vanished scope went into a shared (rule, text) pool, so deleting one
`except Exception:` paid for an unrelated new one elsewhere, and class scopes (never units)
always looked vanished. Hits now transfer only along a unit matched by body hash.
- A unit's prior is now looked up by body hash before qualname, so a TS anonymous callback
whose source-order ordinal shifted keeps its cap.
- The TS body hash included the function name; it is now parameters + body, so a rename keeps
its cap (as in Python).
- HX006 flagged `await asyncio.wait_for(proc.communicate(), timeout=...)`; calls passed to
wait_for/timeout are bounded. The fix text now names the asyncio form.
- scripts/check printed output only on failure, hiding advisories and comment allows; the
health report is now always shown.
Docs now say growth of a file already over 2,000 lines is a warning for now.
scripts/code_health measures the files a change touches on both sides
(base revision and head) and fails when:
- a function or file over target grows past its own value on the base
(CC 20, 300 own lines, nesting 6, file 2,000 lines);
- a new function or file is over target;
- a pattern rule gains a fingerprint (rule + enclosing function + line
text), so fixing one violation never buys another.
Code that moves keeps its cap: functions match by name-independent body
hash, pattern hits by (rule, line text) once their function has left the
old place. Nested functions are their own units, so a closure edit never
counts against the hook or function around it. Python CC is ruff's C901
minus the direct nested defs; TypeScript is parsed with the lockfile's
typescript (no type check).
Rules: swallowed exceptions (S110, BLE001, E722), correctness (F821/F823/
F811/F841, RUF006), hangs (ASYNC230/240, S113, subprocess/urlopen without
timeout, sync config I/O in async def, get_event_loop, gather results
checked against Exception), profile scope (hardcoded ~/.hermes, a new
HERMES_* env var, UnscopedSecretError -> os.getenv, import-time capture,
raw threading.Thread, P05/P06 from profile_scope_patterns.json), argv
process identity, bool() of an env string, and if/elif ladders.
Growth of a file already over 2,000 lines is advisory: replaying 300
merged PRs, a blocking file-size ratchet would have stopped 46 of them
before the 119 over-target files are split. Everything else blocks; the
same replay finds 18 PRs that would have needed a change, every finding
a real hit for its rule.
No baseline file: base is measured on the fly, so an improvement that
lands on main is the new cap at once and parallel PRs never conflict on
ratchet state. `# health: allow <RULE> -- <why>` is the reviewed escape.
The HermesHome/InstallDir guard checked HermesHome first and never looked at
HERMES_RUNTIME_DIR once HermesHome sat inside InstallDir, so the documented
escape hatch could not lift the refusal (Windows lane: store-guard case 4).
The store is HERMES_RUNTIME_DIR when set, else under HermesHome; judge that.
The runtime.ps1 Unicode tests read the helper's answer through a UTF-8 file:
under code page 437/936 the console cannot carry the profile's characters,
so a stdout capture lost them (and the 65001 cell's piped ConvertTo-Json
unrolled the one-element array into a string).
Extends #124526's class fix to the two remaining native captures that can
carry a profile path, and closes the secondary issue from the report:
- scripts/desktop-update/runtime.ps1: the launcher --print-runtime-command
JSON and the legacy user-bin --version 'Install directory:' capture are
both scoped to [Console]::OutputEncoding = UTF-8 and restored in finally,
matching install.ps1's Invoke-Native -Utf8Output and every other consumer
of the boundary (windows_ssh_runtime, the Electron updater, the Rust
bootstrap all decode UTF-8). PS 5.1's OEM decode mojibaked the reported
install root so the legacy identity check never matched on a non-ASCII
profile.
- scripts/install.ps1: -HermesHome equal to or inside -InstallDir puts the
pm tool store inside the checkout; the repository stage's occupied-
directory preflight then refuses every retry after the first run
populated <InstallDir>\tools. Refused up front unless HERMES_RUNTIME_DIR
parks the store elsewhere.
Tests: tests/scripts/desktop_update/test_desktop_update_runtime_unicode.py
(real published launcher, non-ASCII profile, CP936/437/65001, encoding
restore) and scripts/tests/test-install-ps1-store-guard.ps1 (real installer
subprocess through -ShowResolvedPaths: refusal, escape hatch, defaults).
Co-authored-by: 择梦舟 <158067205+dreamfarer-space@users.noreply.github.com>
The libatomic sudo warm-up checked only for a terminal, so
--non-interactive, -NonInteractive and --skip-setup still reached
`sudo -v </dev/tty` inside an SSH session or any PTY-allocating runner.
Gate it on NON_INTERACTIVE like setup and gateway. Reported by
@JoaoMarcos44 on #132664.
The clone and downloads can outlast sudo's timestamp_timeout, after which
PM's sudo -n repair silently falls back to the manual remedy. Refresh the
ticket non-interactively right before PM installs. Compute the bootstrap
target once in stage_prerequisites.
PM's libatomic repair runs `sudo -n` under its install lock, so on a host
whose sudo needs a password the automatic attempt can never succeed and
`curl | bash` still ends in "pm install failed". When the glibc host lacks
libatomic.so.1 and a terminal is attached, cache sudo credentials during
the prerequisites stage so the later non-interactive install goes through.
Root, passwordless sudo, musl, and headless runs are unchanged.
Follow-up to the cherry-picked session handshake, rebased onto the
multiplex bridge ownership that landed in #132518:
- connect(): the foreign-session fatal sits ahead of main's
secondary-aware pidfile/port reaping, so neither path signals a bridge
that serves another profile's session.
- Drop the separate "no session -> restart" branch. A bridge without the
field runs an older bridge.js, so its scriptHash already marks it
stale and it restarts through the existing path.
- Inline sessionIdentity() as path.resolve() in /health and drop its
node test, which only exercised path.resolve and no CI lane runs.
- Collapse the four adapter cells into one parametrized invariant (own
session adopted; another session never adopted or killed, connected or
disconnected). The read-receipt test now reports its own session, so
it still restarts for the read-receipt reason rather than a missing
field.
- Docs: fold the separate-gateway guidance into the existing "Multiple
profiles" section instead of a second multi-profile section.
Co-authored-by: Victor Kyriazakos <victor@rocketfueldev.com>
Two profiles on one host default to the same bridge_port (3000) but have
profile-scoped session paths. _reuse_running_bridge adopted any connected
bridge whose scriptHash and read-receipt config matched, so profile B
adopted profile A's bridge and served A's session; on any mismatch,
connect() fell through to the pidfile/port kill path and killed A's
running bridge.
Contract, keyed on a new /health `session` field (normalized absolute
session dir via bridge_helpers.js sessionIdentity):
1. session present and EQUAL to the adapter's own normalized session
path -> today's logic unchanged (adopt on hash + read-receipt match,
else stale/restart).
2. session present and DIFFERENT -> FOREIGN bridge: not adopted, not
killed. connect() sets a fatal error naming the port, the foreign
session path, and the fix (platforms.whatsapp.extra.bridge_port, one
distinct port per profile).
3. session ABSENT (bridge predates the handshake) -> treated as stale
and restarted, exactly like an absent scriptHash today.
Normalization uses Node path.resolve / Python os.path.abspath - absolute
and `..`-normalized WITHOUT symlink resolution. fs.realpathSync /
Path.resolve() would resolve symlinks asymmetrically (macOS
/tmp -> /private/tmp) and produce false foreign verdicts.
(cherry picked from commit 7e8418678e91fa4bc69d4b4fa6faedd66f2a80e5)
A layout matrix (single/fleet × strip/condensed × local/remote active,
starting from a named profile or the all view) asserts the active
gateway's default is one click away in each. The four fleet+condensed
cases fail on main.
The core E2E lane only starts on PRs touching its guarded paths, so a
profile-rail edit would have met fleet-condensed-default.spec.ts only on
main. Add the rail sources to the lane.
Fleet telemetry labelled every `hermes -z` / `chat -q` / `-Q` run as
entrypoint=interactive because the cli surface mapped straight to
interactive and no caller declared anything else, so 95% of CLI sessions
read as one-turn humans and scripts could not be told apart.
- New closed entrypoint `one_shot` (contract + v3 schema enum). The task
scope start declares it for cli-surface runs carrying the
HERMES_SINGLE_QUERY_SESSION marker the one-shot paths already set (the
signal session source and cache_ttl: auto read). Children Hermes
dispatchers spawn (kanban worker source=kanban, A2A forward source=a2a)
read the existing `background`. Delegated children, other surfaces and
the REPL are unchanged.
- Engagement keeps treating one_shot like interactive (its prior label).
- `hermes -z` exits via os._exit, skipping the atexit hook that closes the
metrics session: its pre-exit cleanup now runs the runtime shutdown, so
task_run.finished and session.count are actually written (before, only
task_run.started survived and the store kept a hot journal).
- Smoke expectations move to one_shot (it drives `chat --query --quiet`).
- relay-shared-metrics.md documents the entrypoint vocabulary.
A login bash reads only the first of ~/.bash_profile, ~/.bash_login and
~/.profile. wire_shell_path always wrote ~/.profile and ~/.bash_profile
when present, so a user whose only login file is ~/.bash_login never saw
~/.local/bin on PATH. Treat ~/.bash_login like ~/.bash_profile: append the
guarded PATH line when the file exists, never create it (creating
~/.bash_profile would shadow it).
The test helper resolves bash by absolute path so the login-PATH tests run
on hosts without /bin/bash (NixOS).
scripts/windows-build-deps.ps1 discovers the ARM64-capable Visual Studio
(vswhere -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64) and
initializes VsDevCmd against it, then builds OpenSSL through vcpkg. But
vcpkg resolves Visual Studio on its own and takes the newest instance. On a
host with a second BuildTools install that lacks the native arm64 toolset,
the arm64-windows triplet fails:
error: in triplet arm64-windows: Unable to find a valid toolchain for
requested target architecture arm64.
The selected Visual Studio instance is at: ...\Microsoft Visual Studio\18\BuildTools
The available toolchain combinations are: x86, amd64, x86_amd64,
amd64_x86, arm64_x86, arm64_amd64
...in the same run that logs "-> ARM64 C++ build tools found: ...\2022\BuildTools".
The install fails, `hermes update` exits 1, the retry fails identically, and
the desktop relaunches on the previous build ("running with the previous
dependencies — run `hermes update` to finish it"), so it never updates.
Export the discovered instance as VCPKG_VISUAL_STUDIO_PATH, the documented
lever vcpkg's own error points at, without clobbering a caller's explicit pin
(same respect-if-set shape as CARGO_HOME/RUSTUP_HOME above).
Extends the initializer fixture: the discovered instance must be pinned when
unset, and a caller pin must survive the call. Needs a Windows runner — the
suite is platforms("windows") and the initializer refuses non-Windows hosts.
`activate` is bash syntax, so fish users had no way to take on the dev
environment. `activate.fish` does what `activate` does: snapshots the keys it
will change, applies the composed env, defines a `hermes` function that
refuses outside the worktree, adds the prompt prefix, and defines
`deactivate`. It shells out to the same bash library, so composition is not
reimplemented.
`pm.environments --format fish` emits the env as `set -gx` statements.
fish's read-only names (`PWD`, `SHLVL`, `_`, ...) are skipped because
`set -gx PWD` aborts the script, and values named `*PATH` stay lists.
`hermes_compose_env` takes the dialect (`sh` or `fish`) as its second argument.
`activate` doubled as the thing scripts sourced non-interactively, so it
carried `--` argument guards and every caller had to tolerate its prompt and
`hermes` function. Composition (setup sync, bootstrap Python, pm env) now
lives in scripts/_activation.sh, and there are two consumers:
- `activate`: interactive, reversible (`deactivate`).
- new `scripts/run-in-hermes-env CMD...`: applies the env to one process and
execs into it. Replaces `scripts/_hermes-python`; every Python script's
shebang and `scripts/run_tests.sh` (which re-executes itself under it,
however it was launched) use it.
`pm.environments --format sh` emits the env as shell exports, so the inline
JSON-to-export Python is gone from `activate`. An install that did not build
the test environment now reads as stale for every consumer.
Tests are split along the same seams: activation support fixtures, bash/ps1
activation, env-script quoting, and the runner's staleness.
#131679 set maintenance.auto=false in the treeless install checkout to stop the
commit-graph lazy-fetch loop (#127711). That also switched off the post-fetch
`gc --auto` that git <= 2.53 runs after every lazy fetch, so packs piled up
between updates and only folded on a manual `hermes update` / `--check`
(nothing runs `--check` automatically).
The loop needs a commit-graph write, and gc.writeCommitGraph=false +
fetch.writeCommitGraph=false already stop gc and fetch from writing one.
maintenance.commit-graph.enabled=false closes the last writer (a commit-graph
maintenance task, e.g. from a user's global maintenance.strategy) while
leaving automatic maintenance, and its gc fold, on.
Checkouts that already got maintenance.auto=false from the first cut are
converged: the next update/--check unsets exactly that value.
Measured on a real tree:0 clone with a Bloom commit-graph, 100 lazy fetches
after one updater run:
git 2.53.0: main 101 packs, this change 1 pack, 0 commit-graph writes
git 2.55.0: 101 / 101 (stock 2.55 maintenance does not fold), 0 writes
Path-based review (workflows, composite actions, eslint/prettier config,
the MCP catalog, install-hook files) moves to .github/CODEOWNERS. GitHub
requests and enforces that review, so CI no longer classifies those paths,
checks a label, or reruns failed jobs when the label is added.
Removes review-labels.yml, emit_review_status.py, the rerun-review-labels
job, and the ci_review / ci_review_files / mcp_catalog classifier outputs.
The run-e2e rerun job in label-rerun.yml stays.
The supply-chain scan keeps its content patterns (base64 decode + exec,
obfuscated subprocess), which CODEOWNERS cannot express. It fails on its
own again, as it did before #68833 routed it through the label gate. Its
.pth and install-hook path checks are dropped in favour of CODEOWNERS.
The two salvaged commits above disagree: #127721 persists gc.auto=0, which
turns the fold's `gc --auto` into a no-op (106 packs stay 106 on git 2.50.1,
2.53.0 and 2.55.0), and the fold itself is exposed to the loop #127721
guards against. Probed on all three gits against a blobless clone whose
commit-graph carries changed-path (Bloom) data:
- gc, `gc --auto`, `maintenance run --task=commit-graph` and
fetch.writeCommitGraph each lazy-fetch the trees of every commit the graph
has not seen (30 unseen commits: 30 fetches, 30 packs), and every lazy
fetch spawns `git maintenance` again. gc.writeCommitGraph=false stops it
(gc: 31 packs -> 1).
- Without a Bloom graph none of that happens, so the keys are a guard, not a
fix for every partial clone.
Changes:
- keys: maintenance.auto, gc.writeCommitGraph, fetch.writeCommitGraph; gc.auto
stays default. Idempotent (no rewrite when already set), `--local`, hidden
console on Windows. install.sh and install.ps1 set the same three keys.
- consolidate_lazy_fetch_packs applies the keys before the fold, so installs
that predate the installer change converge on their next update check.
- the fold runs under bounded_probe_run: subprocess.run(timeout=) kills only
`git gc` and leaves `pack-objects` running (reproduced), and with
GIT_NO_LAZY_FETCH.
- dropped the post-gc mark_unmarked_packs_promisor: gc keeps the .promisor
marker on 2.50.1, 2.53.0 and 2.55.0.
- docs: updating.md troubleshooting entry.
`release.py abandon` deleted the draft and wrote the marker but left the
attempt's Stable Release run going. A live run keeps building, and with
autopublish on it can publish the draft that abandon just cleared.
abandon now lists the claim ref's runs of stable-release.yml and runs
`gh run cancel <id> --force` on every one that has not completed. This
happens before the draft is deleted and before the marker is written: the
marker removes the attempt from the outstanding set, so a cancel failure
after it could never be retried. A published release is still refused
before anything is cancelled. The `delete` callable is renamed `execute`
since it now runs cancels too.
The MSIX package carried a single Square44x44Logo.png, so every taskbar and
Start slot on a HiDPI Windows 11 machine was Windows upscaling a 44px bitmap,
and with no altform variants Windows plated the icon itself. The generator now
emits the qualified catalog Windows resolves by itself: <Logo>.scale-{125,150,
200,400} for all four manifest logos at Microsoft's pixel sizes, and
Square44x44Logo.targetsize-{16..256} in the three required theme forms —
unqualified and altform-lightunplated carry the light tile, altform-unplated
(dark theme) the dark tile, the same split macOS dark mode gets. before-build
stages the whole assets/appx directory; app-builder-lib runs makepri and
`makeappx /l` as soon as it sees a qualified asset, so nothing else changes.
The hand-listed *-dark.png logos, generated but never staged, are gone.
Exposed along the way: the commit badge sat outside the portrait's silhouette
clip, and at 16–30px its anti-aliased edge stacked coverage on the plate's
corner pixels and changed the alpha channel between build flavors. The badge
now shares the clip, is plain black, and is left off direct renders below
32px where no SHA is legible anyway.
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the Icon Composer package, and
dmgbuild's badge option can only paste onto the stock removable-drive icon.
Ship our own drive-with-the-girl artwork instead: assets/dmg-volume.png
(hand-made, one artwork since volume icons have no appearance variants) is
emitted as apps/desktop/packaging/dmg-volume.icns, beside the DMG background
so the `files` whitelist keeps it out of the app bundle, and dmg.icon points
at it.
Tauri always uses the app icns as the volume icon and offers no override, so
the bootstrap workflow runs scripts/bundles/set_dmg_volume_icon.sh on the
built DMG before signing (the same .VolumeIcon.icns + custom-icon flag
dmgbuild sets), and the verify step checks the icon is on the mounted image.
Verified on macOS 26.6.2: both DMGs render the identical volume icon.
Tauri's bundler (cli 2.11.4) compiles an Icon Composer package listed in
bundle.icon into Assets.car with actool >= 26 and sets CFBundleIconName, so
the bootstrap installer gets an unbranded twin of the desktop package under
src-tauri/icons/icon.icon (generator target; the bundle stays the plain
brand because flavours only apply under apps/desktop/). Tauri only logs when
actool is too old, so the macOS job selects Xcode 26 and the verify step
asserts Assets.car and the plist key are in the built .app.
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the .icon package; point
dmg.icon at the full-resolution assets/icon.icns. Tauri's DMG already takes
--volicon from the bundle's .icns, which the regenerated outputs cover.
Next to macOS 26's own icons the flat plate + girl reads better than the
ringed tile, so the ring is off in every output: the tiles' inward stroke,
the layered icon's border layer and its targets. BORDER_ENABLED keeps the
ring code one flag away.
Without a ring the girl's lowest nodes are dragged past the tile edge
(EDGE_OVERSHOOT) so the outline crops her instead of leaving a gap.
Clear light/dark and Tinted light/dark now come from one grayscale mono
layer with Liquid Glass on: the parts of the girl that are black in light
mode become a near-black frosted ink (0.13 @ 90%), her white parts stay
white, each pixel painted exactly once so materials never stack. The art
layer hides under "tinted" and the mono layer shows only there.
Mac girl at 1.06x (between main's 1.12x and the layers' 1.0x), for the icns
and the layered icon alike. Chosen from a six-appearance matrix of real
IconServices renders next to Finder.
main scaled the girl 1.12x for every mac target (a white tile with a ring
reads small beside full-colour peers), but the macOS 26 layers were still
registered on the full-bleed squircle box, so macOS 26 would have shown a
smaller girl than macOS 15.
The layered icon's canvas is the plate itself, so derive its portrait box
from the mac box in plate coordinates. Ink overlap with the icns girl goes
from 0.65 to 0.996 IoU; only the two art layers change.
Clear light/dark and Tinted light/dark all render from one "tinted"
specialization: the system reads the layer as luminance (white = visible)
and tints it. Without one they fall back to the light layers, whose black
girl and ring have no luminance, so those four modes showed an empty plate.
Point "tinted" at the white layers already used for dark mode; the manifest
test now requires it and pins it to the dark image. Verified in the Dock on
macOS 26.6.2 in Clear and Tinted, light and dark: white girl and ring as
glass / in the tint colour.
Each layer carried both a fixed "image-name" and per-appearance
"image-name-specializations". actool treats the fixed name as the image for
every appearance and drops the specializations, so Assets.car held only the
light PNGs and dark mode showed the black girl and black ring on the dark
fill: a black squircle. Icon Composer itself writes one or the other, never
both.
Drop the fixed name; the manifest test now rejects it and requires a dark
specialization on every layer. Verified on macOS 26.6.2 in dark mode: the
catalog gains art-dark/border-dark and the Dock shows the white girl.
macOS 26 clips every app icon to its own squircle and shows legacy .icns art
inside a glass plate. Our border is a stroke on the tile's rounded rect, so on
26 the ring sits inside a grey rim and thins toward the corners where Apple's
curve and ours diverge.
Add an Icon Composer package (apps/desktop/assets/icon.icon) of canvas-filling
layers the system masks itself: a border band whose outer edge is the canvas
(the mask supplies the outline) and whose inner edge is Apple's mask offset
inward by the brand border thickness, so the band is the same width everywhere
along the visible outline; and the girl (+ commit badge) as a second layer.
Build flavours recolour the fill in icon.json only.
Apple's mask is a circular arc flanked by two cubic easing segments (the
Figma corner-smoothing construction). Fitted to macOS 26.6.2's own render of
a system icon on the 824-on-1024 grid: radius 214px, smoothing 0.645, rms
0.32px / worst 0.81px; a superellipse cannot get under 0.54px rms.
The .icns targets are unchanged: Apple's own 26-era .icns still sit on the
824 grid, and macOS <= 15 keeps showing that artwork.
The new contract test probes the border layer 640 points along the mask:
opaque at thickness-1.5px, transparent at thickness+1.5px. Rendering the
layers with the old r=185.4 corner violates it at 1448 probe points.
contextlib.suppress swallows a failed import exactly like a non-raising except
arm; the audit marked such imports bare, so a module only ever imported under
suppress(Exception) by updaters could never be deleted.
The runner's own probe tests start runners from inside a test process,
which inherits HERMES_TEST_SLICE from the CI slice job. Slice 1/2 then
ran half of each probe's file set and slice 2/2 ran none of a one-file
probe ("probe never wrote handoff file"). Read the slice once and drop
it from the environment.
The full suite was the last job to finish in 33 of 43 CI runs since it
moved from a 96-core to one 32-core runner (p50 666 s). That runner is
~90% packed, so only more machines help, and one bigger box is poor
value: on 96 cores the same files took 25-50% longer each and the suite
ran ~1.8x faster for 3x the price. Two 32-core slices scale linearly
(simulated from per-file CI timings: 262 s / 252 s vs 514 s on one box).
macOS (free 3-core runner, ~7.5 min) would become the slowest required
job once Linux splits, so it runs as two slices on the same free runner.
--slice now partitions from the checkout alone (sorted round-robin):
each slice job restores its own duration cache, and a main push saving
a newer cache between the two restores made timed LPT compute different
partitions per job, silently dropping or double-running files.
Round-robin lands within 5 s of timed LPT at this suite size. Each slice
saves its own cache key on main; the runner keeps the restored entries
for files outside its slice, so the cache stays complete.
The 124 remap was vetoed only by "Update not complete", but the restart
and verify phase prints other failure lines after "Update complete!":
"Update incomplete — …", "✗ <unit> failed to come back/stay running
after restart", "verification incomplete". Any of them now keeps the
watchdog kill a failure. Only text after the last banner is inspected,
and ✗ is matched as \u2717 because Windows PowerShell reads the BOM-less
script as ANSI.
Reported by @unsupportedpastels in review.
The Windows hand-off's idle watchdog is the only producer of exit 124 on
main (posix.sh has no watchdog and `hermes update` never exits 124), so
the posix remap was dead and goes. The retry-state helper and its
self-test arm collapse into one Resolve-HermesUpdateOutcome check, now
proved end to end through the real hand-off instead: a fake update that
prints its banner and parks silently past a 3 s ceiling surfaces as a
verified success with the gateway fleet restored.
The remap also refuses a run whose output already carries the
"Update not complete" verdict, which supersedes the banner.
Port of #96340 (Finn763) onto the current legacy-scoped retry gates,
narrowed to the watchdog sentinel only: exit 124 + the "Update complete!"
marker maps to success so the hand-off verifies and relaunches the new
build instead of re-running the completed update (the 70+ minute popup
retry storm). A plain exit 1 after the marker is the stale-gateway
verdict (hermes_cli/update_receipt.py) and must keep failing.
As first written this also added a posix.sh gate and a retry-state
helper with a -SelfTestRetryState arm; the next commit drops the posix
gate (nothing there exits 124) and replaces the helper and self-test
with one end-to-end test through the real hand-off.
Co-authored-by: Finn763 <165816600+Finn763@users.noreply.github.com>
Submitters only saw a stale 5-point summary on the catalog page (it even listed
a 'Released' requirement nothing enforces), while reviewers applied ten README
rules plus criteria written nowhere public (credentials, approvals, disclosure,
compatibility metadata, forks of bundled plugins).
- plugin-catalog/README.md: rules 11-15 write down those review criteria; the
rule list sits between admission-rules markers.
- New developer-guide page 'Submitting to the plugin catalog': before you
submit, the PR flow, the full rule block (verbatim mirror), pin updates,
delisting vs removal, asking for a hook.
- The /plugins 'Submit' links and the user-guide section point at it.
- tests/website/test_catalog_rules_mirror.py fails when the two rule blocks
differ; classify_changes makes a README-only edit run it.
- AGENTS.md, plugins/AGENTS.md and CONTRIBUTING.md reference both files.
- Run the data-only guard once, inside the changed-entries step of the
pinned-source job, and only when an entry YAML changed. README, policy,
docs and CI PRs that touch plugin-catalog/ without an entry no longer go
red.
- Allow contributors/emails/** beside plugin-catalog/**: the attribution
fix we push onto catalog PR branches is data the gate never executes
(29 of the last 134 merged catalog PRs carried one).
- Diff GitHub's merge commit against its first parent with fetch-depth: 2
instead of a full-history clone plus merge-base. The structural job,
which only validates the whole directory, goes back to a shallow
checkout with no diff at all.
- `subdir:` with no value means the repo root in both the parse step and
the structural validator, matching how the runtime loader has always
read it.
The pinned-source job joined a PR-controlled subdir onto the clone root
with no validation, accepted file:// repos and ref names as the "pin",
and followed committed symlinks out of the clone, so the gate could
validate attacker-planted files instead of the declared commit's
contents. Harden the step end to end:
- validate repo (https://, whitespace-free), sha (40-hex, commit object),
and subdir (plain relative path) in the parser before any use
- run the parser under python3 -I against installed ruamel.yaml so the
PR checkout cannot shadow its imports, capture parser output, and
reset shell variables per entry so a dead parser cannot replay the
previous entry's fields
- confine PLUGIN_DIR and every symlink beneath it to the clone root
- include renamed and typechanged entry files in the changed set
- feed entries over fd 3 and keep plugin-executed commands off stdin so
validate cannot drain the remaining list
- NUL-delimit both self-updater grep stages and sanitize the echoed
filename list so crafted names cannot forge workflow commands
- mirror the same repo/subdir rules in the runtime catalog loader and
the site extractor, and use \Z anchors so a trailing newline cannot
split the verdict between gates
WIP for the freshness-guard-os-metadata unit: the UpdateLock claim in
complete_source_checkout plus its tests are complete but unrun; the
Thumbs.db/Desktop.ini skip predicate is in but its test rows are not yet
extended, and the rmTree ENOTEMPTY test has not been written yet.