Skip to content

Ergopti v1.0.6

Choose a tag to compare

@github-actions github-actions released this 23 Oct 08:24
Immutable release. Only release title and notes can be modified.

Ergopti 1.0.6

Ergopti — keyboard layout: install the ergonomic layout without running an application.

Ergopti Plus — advanced typing tools: the companion application adds remapping, shortcuts and text expansion.

Downloads

Ergopti — keyboard layout only

No Ergopti Plus application is required for these keyboard layouts.

Platform Download Installation
Windows Ergopti_windows.exe Run as Administrator, log out and back in, then select the layout in Settings → Time & Language → Language & region.
macOS Ergopti_macOS.zip Unzip, copy the .bundle folder to /Library/Keyboard Layouts/, log out and back in, then add it in System Settings → Keyboard → Input Sources.
Linux Ergopti_xkb.zip Follow the XKB installation guide. Includes standard, ANSI, plus and plus-ANSI variants.

Ergopti Plus — application with advanced typing tools

Select your platform and download the corresponding file.

Windows — Ergopti Plus

Asset Description
ErgoptiPlus.exe Compiled AutoHotkey executable (AHK v2 runtime bundled)

Installation — app:

  1. Download ErgoptiPlus.exe.
  2. Double-click to run. On first launch, resources are extracted to %LOCALAPPDATA%\Ergopti.
  3. Your personal configuration is stored in %USERPROFILE%\.config\ergopti_plus. If this folder does not exist yet (first launch), the setup wizard will open automatically.

macOS — Ergopti Plus

Asset Description
ErgoptiPlus.app.zip macOS application (Hammerspoon bundled; Karabiner-Elements installed on first run)

Installation with Homebrew:

brew tap adrienm7/ergopti
brew install --cask ergoptiplus

ergoptiplus installs the stable releases and ergoptiplus@dev the dev prereleases; this release is in ergoptiplus. brew upgrade then updates it with your other casks, and the app still updates itself from its About menu.

Installation — app, by hand:

  1. Unzip ErgoptiPlus.app.zip and move ErgoptiPlus.app to /Applications.
  2. Remove the quarantine flag (the app is not yet Apple-notarised):
xattr -dr com.apple.quarantine /Applications/ErgoptiPlus.app
  1. Launch the app. On first run, Karabiner-Elements will request a System Extension approval — required for key remapping.
  2. Your personal configuration is stored in ~/.config/ergopti_plus/. If this folder does not exist yet (first launch), the setup wizard will open automatically.

Linux — Ergopti Plus

Asset Description
ErgoptiPlus-linux-amd64.deb Driver package for Debian, Ubuntu and Mint
ErgoptiPlus-linux-x86_64.rpm Driver package for Fedora, RHEL and openSUSE
ErgoptiPlus-x86_64.AppImage Portable driver — any distribution, no root required
ErgoptiPlus-linux-x86_64.flatpak Sandboxed driver, for a distribution-agnostic install
ergopti-plus-linux.tar.gz Driver bundle with install.sh, installs under your home directory

Which file for your distribution:

  • Debian, Ubuntu, Mint, Pop!_OS, elementary → ErgoptiPlus-linux-amd64.deb
  • Fedora, RHEL, CentOS, openSUSE, Mageia → ErgoptiPlus-linux-x86_64.rpm
  • Arch, Manjaro, EndeavourOS → build it from tools/build/PKGBUILD in the repository; no Arch package is attached to this release
  • Flatpak → ErgoptiPlus-linux-x86_64.flatpak, installed with flatpak install --bundle. It requests --device=all, which is the only permission that reaches /dev/input and /dev/uinput; the sandbox still cannot grant access your user does not have on the host, so the udev rule in step 2 is required either way
  • Any other distribution, an immutable one, or no root access → ErgoptiPlus-x86_64.AppImage or ergopti-plus-linux.tar.gz

Alpha. CI exercises virtual input devices, display sessions and package launches. Physical keyboard behavior and desktop-shell integration still need validation on the target distribution.

Installation — app:

  1. Open the downloaded package with your distribution's software installer and install it.
  2. Open Ergopti Plus from the applications menu. Input permissions are prepared by the package; if additional authorization is needed, Ergopti opens a graphical administrator prompt.
  3. Follow the first-use window. If group membership changed, sign out and back in when prompted.
  4. Choose Global actions → Start at login to enable or disable automatic startup.

Removal: choose Global actions → Uninstall Ergopti, or remove the package through your software manager. The standalone archive also includes an ownership-checked uninstall script. Personal settings are preserved.

Changelog (click to expand)

Timestamps are in Paris time (Europe/Paris, CET/CEST).

Commits since v1.0.5.

Features

  • Linux: Recommend what Windows recommends, CapsWord on chords
    The maintainer decided on 2026-10-22 that restoring the recommended
    values on Linux gives the Windows preset. LAlt then CapsLock now
    recommends caps_word; the three- and four-finger gestures and the
    four-finger tap recommend the Windows bindings; the WPM pill is neutral
    as on Windows. Fresh configurations still start neutral. Two values stay
    different, and the gate proves why: the four-finger vertical swipes stay
    unassigned because win_app_prev and win_app_next do not exist on Linux,
    and the number row stays native because Linux only reports the layout's
    own row.

    CapsWord is an executor action that locks CapsLock, so the engine's own
    per-letter mode had no production caller. It is removed: its engine
    owner, the hook's arm/plan/ack ports and output custody, the native
    semantic adapter, the shared policy and their tests. Simultaneous chord
    slots, which refused caps_word because only that mode could run it, now
    hand it to the executor like a pair tap.

    test-defaults-single-source compares every recommendation both drivers
    share and rejects the former LAlt then CapsLock value; the key-catalogue
    gate fails if the hook regains arm_caps_word; Lua tests publish and fire
    a CapsWord chord through the real picker and manager executor. One
    commit because the key-catalogue gate pins all three changes and the
    pre-commit hook refuses a partially staged file.

  • Linux: Copy the selected files' paths in a file manager
    copy_selected_path only opened the primary selection on Linux, while
    Explorer and Finder copy the selected files' paths. In a known file
    manager (Nautilus, Dolphin, Thunar, Nemo, Caja, PCManFM...) it now
    copies the selection through the clipboard's text/uri-list read,
    decodes the file: URIs through _shared/lua/file_selection and leaves
    the paths in the clipboard, one per line, with the shared "Path
    copied" notice. No file selected falls back to opening the primary
    selection, as on macOS; an unreadable or remote selection is refused
    without copying part of it. Ctrl+C goes to file managers only, since a
    terminal would interrupt its program, so GNOME and KDE under Wayland,
    which name no focused application, keep the opening behaviour.

  • Open the shared action picker for the Windows and Linux hold
    The maintainer decided on 2026-10-22 that a tap-hold key's hold and a key
    combination's hold slot open the shared action picker on every system, as
    macOS does since 61ab0fb and 1eb85e2. Windows and Linux still
    unfolded a submenu (a list dialog for a Linux pair) of the hold options.

    The picker lists what a hold can be: the options of [tap_hold.hold_picker],
    every modifier and combination and the navigation layer, without the none
    sentinel, which the picker's own none row stands for. Windows builds them in
    TapHoldHoldPickerItems, Linux in hold_options.picker_items, from the same
    shared catalogue. The tap_hold_key_hold group is replaced by the one command
    row tap_hold_key_hold_action on the three drivers.

    A pick goes through the same owners as the submenu did (TapHoldSetHold with
    the configuration folder, SetKeyCombinationHold, tap_hold_writer.set_hold,
    the pair scope edit). An id the hold does not offer, or a refused write,
    keeps the picker open: on Windows ShowActionPicker gains a slot mode that
    commits the pick while the window is open and reloads only after it closed;
    the Linux bridge already kept a refused pick open. The Linux titles use the
    hold presentations, which now declare linux.

    The group-row caption tests move to the agent's System 1 group, the last
    caption group Windows and Linux share.

  • Macos: Count unmatched, late and stuck key events per session
    Issues #99 and #100 report swallowed keys and an accent popup on a
    plain tap, and no log line could say whether a key-down or a key-up
    went missing, or whether the taps saw them late.

    infra/key_event_health.lua counts what the keymap keyDown and keyUp
    taps see of physical input: key-ups with no key-down, key-downs whose
    key-up was lost, keys silent for 3 s while down, character keys held
    500 ms or more for macOS (its own event stamps, with the autorepeats
    and the rule switch they crossed: a lease fence, a karabiner.json
    reload or a tap outage), callbacks 250 ms or more late, and Karabiner
    lease failures by kind. Lines are written off the tap, coalesced per
    run-loop turn, and carry the session totals; no key code or character
    is ever logged. adapters/event_clock.lua measures lateness against the
    freshest recent events, so the two clock domains (raw ticks on Apple
    silicon) and any constant offset cancel out. classify_with_fence names
    a physical replay so its copy is not counted twice.

    macOS only: the Windows and Linux reports have not shown this class.

  • Updater: Hand a feed-only release to Sparkle from Versions
    When the GitHub API gives the Versions window no list, the window reads
    the releases Atom feed, which names no files and no digests, so a direct
    install is refused with the no-details warning and nothing could be
    installed from the window at all.

    On macOS, Sparkle verifies a release on its own against the EdDSA
    signature of the channel appcast, which carries that channel's newest
    release only. The page now offers, for a feed-only list, an install of
    the channel's newest release when the shared offer rule offers it; the
    click posts install_through_updater, the window checks the tag again
    against the feed it read and asks Sparkle to check that channel through
    modules/updater/feed_handoff.lua, and the page shows the new handed_off
    phase while Sparkle's own dialog asks the user. Older rows keep the
    existing warning. The hand-off lives in its own module so every file
    that asks Sparkle for an update still owns no install primitive.

    Windows and Linux seed no hand-off: neither has an updater that verifies
    a release without the API list (the Windows exe is unsigned on the dev
    channel and may be on stable; the Linux tarball has no signed update
    metadata and its .sha256 is only an asset the API names), so their
    feed-only lists keep the no-details warning, stated where it is raised.

    The three page strings landed in all 21 locales with 7b01b5f.

  • Metrics: Label typing data built from an incomplete history read
    The maintainer chose to show the counts that a failed SQLite read still
    produced instead of refusing the whole history. The shared dashboard
    now takes an incomplete flag per part, the manifest and the n-gram
    range, from macOS publications and from Windows and Linux blobs, and
    the freshness banner says the history is incomplete until both parts
    arrive complete again. The notice is localized in all 21 locales.

  • Menu: Restore and clear each keyboard modifier group
    Each modifier submenu of the keyboard shortcuts (Alt, Ctrl, Ctrl+Shift,
    Win on Windows; Option, Ctrl, Ctrl+Shift, Cmd, Cmd+Shift on macOS; Ctrl,
    Ctrl+Shift, Alt, Super on Linux) now opens with the shared « Restaurer
    les valeurs conseillées » and « Tout effacer » rows, then a separator.

    One manifest fragment, keyboard_group_scope_controls, declares them on
    the three drivers with the two existing locale keys. Both narrow the
    Shortcuts scope to the group's slots and their action parameters: the
    restore puts the shipped slots back to their recommended action and
    removes the user's additions, the clear empties the group. Like every
    scope row they apply at once, after a backup, without a question.

Fix

  • Ci: Read a run listing again when it shifts between pages
    Run 39878950422 failed the macOS build with "Duplicate qualified managed
    Ollama inputs". The run listing is read 50 records a page, newest first;
    an artifact uploaded between two page reads pushes the last record of
    page 1 onto page 2, so one artifact came back twice. listAll now reads
    such a torn listing again from its first page, up to five times, and
    fails when it keeps shifting. Two records with distinct ids still reach
    the duplicate refusal.

  • Linux: Name the lost-click notice title before sending it
    c280aa4 added the notice for held tray clicks the refreshed menu
    cannot match to ui/menu/rebuild_owner.lua. Its notifier options wrote
    title = I18n.get(...), which the menu-row census of ui/menu/ reads as
    a static row built outside the manifest, so both menu-row ratchets
    failed (linux 1, baseline 0). The title is read into a local first, as
    local_model_offer does; the notice is unchanged.

  • Linux: Rebuild the tray menu after a hotstring reload
    A reload (tray row, SIGHUP, magic key, local reload control) re-read
    every source but never asked for a menu rebuild, so the pre-reload rows
    stayed live: counts were stale and a click made against an old
    checkmark toggled the reloaded state back. hotstrings_config.reload now
    notifies its change owner, which retires those rows; clicks made on
    them replay on the refreshed menu with the state they asked for.

  • Linux: Replay a tray click on a retired row on the refreshed menu
    A refresh request retires every published row at once, but the desktop
    shell keeps showing that tree until the next pump publishes a new one.
    A click made in that window (a second event in the same tray pump, a
    click queued while a synchronous reload ran, a refused publication)
    returned false and was dropped without a log line.

    The rebuild owner now holds such a click under its row path with the
    state it asks for (menu.click_intent) and replays it once against the
    next acknowledged tree, never through the retired closure. A row that
    already shows the requested state is skipped and logged, a row the new
    tree lacks is logged and reported with the localized notice, and a
    click dropped at shutdown or past the hold limit is logged.

  • Macos: Hold menubar clicks during a reload for the reloaded menu
    The static menubar stays clickable through the whole controlled reload
    (lease fence, input drain, teardown), while run_exclusive refuses every
    save there. A toggle clicked then reached the log only: some rows rolled
    back, the hotstrings master stayed switched in RAM, and the reload
    discarded it. A click on the cached tree during its refresh delay also
    flipped the state the stale checkmark no longer showed.

    Every generated row now runs through ui.menu.click_gate. A click made
    while a terminal transition is pending is held; the menu teardown step
    hands it to the successor through the settings store when the reload
    proceeds, logs it dropped on a quit, and runs it in place when the
    reload goes away, with a bounded wait. The successor consumes the record
    once, rejects a malformed or expired one, and replays each row by path
    after the menu prime. A click on a dirty tree resolves on a fresh one.
    Each click carries the state it asks for (shared menu.click_intent, as
    the Windows replay schema v3), so a late click whose row already shows
    it is skipped and logged; an unmatched row gets the localized notice.

  • Ci: Fail a stalled Ubuntu mirror download within 30 s
    Run 39850037912 lost the desktop-webview row twice the same way: the
    package lists came at 14 MB/s, then one 9 MB package sat on a stalled
    azure.archive.ubuntu.com connection for 9.5 minutes until apt gave up
    on it, and the 10-minute install step timed out. Earlier runs today
    lost other Linux rows to the same stall.

    The apt prefix shared by every CI install now sets
    Acquire::http::Timeout=30 and Acquire::Retries=5, so a stalled
    connection fails in 30 s and the retry takes the next mirror the
    runner lists, instead of holding the step.

  • Linux: Ask Ollama for no model while the AI is off
    Every tray build called the model rows provider, which read the Ollama
    inventory (a GET of /api/tags behind a deferred timer) whatever the AI
    switch said, so a Linux boot with the AI off still probed Ollama. The
    provider now lists installed models only with the AI on; the model
    browser row stays reachable. The model-list cases that relied on the
    read with the AI off now say the AI is on.

  • Macos: Keep the AI inert while it is switched off
    With the AI off the macOS driver still bound every profile shortcut,
    resumed the MLX download a reload interrupted, and ran ollama list
    from the models manager constructor and from every menu build. Windows
    and Linux wait for the switch-on.

    The startup controller now logs the Windows line and returns before
    the reattachment and the shortcut restore when the AI boots off. A
    shortcut chosen while off stays a saved preference without a native
    hotkey, and the Ollama manager neither pre-warms nor refreshes its
    installed-model list. The switch commit arms the profile hotkeys and
    the waiting reattachment at the switch-on and disarms them at the
    switch-off; a startup that turns the AI off releases the hotkeys too.

    Fixtures that exercise binding, reattachment or the warm-up refresh
    now say the AI is on, the premise those behaviours require.

  • Ci: Admit a Unit lot whose job record stays in progress
    Run 39836200270 refused the Common Unit admission of Shared UI
    rendering because the API still reported Integration tests (Packaging
    and boot) as "in_progress" with no conclusion, half an hour after every
    one of its steps, the runner's "Complete job" included, had completed
    with success and its dependants had started. The record is never
    refreshed, so the re-read added in 4487bce cannot help.

    Such a job is now admitted on its step records: it must be in progress
    with no conclusion, end with a successful "Complete job", and every step
    must have completed with success or a skip. A queued job, a missing or
    failed "Complete job" or any failed step still refuses.

  • Linux: Refuse an over-ceiling injector pause without FFI (L105)
    Only the FFI nanosleep path checked the shared timer ceiling. On a
    runtime without FFI the injector handed 2^53 + 4096 ms, NaN or an
    infinite pause straight to luv.sleep, or forked the sleep command with
    it: a wait that never ends taken as an ordinary inter-phase pause.

    sleep_ms now refuses any value outside NumberPolicy.within_timer_ceiling
    before choosing a fallback, so FFI, luv and the sleep command share one
    admission. The regression test reaches the pause through the selected
    output ports and fails on the old code for both fallbacks.

  • Lua: Keep backslash bytes in shared and Linux POSIX paths
    A backslash is an ordinary filename byte on macOS and Linux, but the
    shared Lua both drivers load and the Linux driver still split, trimmed
    and walked paths with their own [/] classes. A home folder "/home/a"
    redacted the different folder "/home/a\b" as "~\b", a reloaded file
    "back\slash.toml" was reported as "slash.toml", and a configuration,
    shared or checkout folder ending in a backslash lost that byte.

    The runtime log, diagnostic snapshot, reload gate, extension scan,
    evdev loader, layer files, layer preset and driver control folder, plus
    the Linux manifest reader, tap-hold scope and manager and webview root,
    now go through _shared/lua/native_path.lua. The two Linux bootstraps
    that run before the shared tree is on package.path split at slashes
    alone, as the macOS init.lua block does.

    The separator guard now reads _shared/lua and linux/ beside macos/.
    Genuine Windows-path parsers are listed with a reason (program_parameter
    drive and UNC checks, the redaction of both slash spellings, open_target
    Windows-only targets, the portable backup name check). Four sites stay
    in the baseline because concurrent changes own their files:
    config_migrate, config_unused_keys, llm/profile_selector and the Linux
    keylogger sqlite_writer. Windows AHK path handling is out of scope.

  • Linux: Read the xclip sentinel back as no file selection
    The file-selection probe writes a text sentinel before Ctrl+C and
    expected a uri-list read to fail when nothing was copied. wl-paste does
    refuse the missing target, but xclip answers every target with the text
    it owns: measured with xclip 0.13 under X11, the read returned the
    sentinel with status 0, so make_executable_selection logged an
    unreadable selection instead of saying no file was selected. A read
    equal to the sentinel now means no file selection.

  • Linux: List X11 windows with their application in one query
    The excluded-applications picker reads WindowInfo.getAll(), which ran
    xdotool search and then one synchronous getwindowname per window while
    the menu waited, and never set appId: on X11 the picker offered no
    application at all. getAll() now runs a single wmctrl -lp, names each
    window by its process from /proc (the identity getFocused() reports)
    without a spawn per window, logs a missing wmctrl once and a failed
    query as an error. The .deb and the Nix wrapper now carry wmctrl, which
    the workspace and show-desktop actions already used.

  • Linux: Take the instance lock for every daemon launch
    launch.sh took its flock only for --tray and --session-start, so a
    terminal launch through the packaged command started a second daemon
    next to the tray one, both grabbing the keyboard. Every launch that
    starts the daemon now takes the lock; --help stays lock-free. A
    refused launch says the session lock is held: a duplicate session
    start still exits 0, a direct launch exits 75.

  • Macos: Decode each key-down once and log key pairing off the taps
    macOS CI showed the key pairing counters cost the hot path more than the
    suite allows: the keyDown wrapper read the key code a second time, an
    anomaly line armed a timer from inside the event tap, and the stale
    verdict widened onKeyDownRaw's pinned (event, provenance, status) shape.

    The wrapper now hands the code it read and the stale verdict to
    onKeyDownRaw through one context table cleared after every call, so each
    key-down is decoded once and the raw handler keeps its signature; the
    keyUp tap reuses its code the same way. key_event_health no longer arms
    timers: the keymap tap watchdog (every second) and a lease failure flush
    its queued lines. The audit-v5 guard follows the one decode site.

  • Macos: List the event clock adapter in the diagnostics wiring
    Every adapter must carry an ADAPTER_SPECS entry so the healthcheck can
    report whether it is wired; adapters/event_clock.lua, read by the keymap
    taps and the physical fence, had none (adapter wiring meta test).

  • Macos: Keep the lease worker fed while its answers are unread
    25411ee stopped failing a lease on an on-time deadline while an
    Ergopti menu kept task output unread, but Lua sends the next PING only
    once it has read the last PONG, and the worker's inner fences itself
    after 17.5 s without a private command. A menu open that long still
    lost a healthy lease (exit 73), switched every Ergopti rule off under
    the fingers and redeployed karabiner.json (issues #99 and #100). A
    grace that ran out after a modal window also failed without a read
    turn, and an answer read after the verdict was discarded silently.

    While the output-turn probe is pending, the lease writes a keepalive
    PING at the deadline and on every heartbeat tick (READY gets its own
    cadence), never two within the outer's 2.5 s command budget, which
    refuses an overlapping PING. Their PONGs are consumed without a
    deadline. The grace expiry goes through the same probe, its ceiling
    rises to 120 s now that the worker stays fed, and a late answer is
    logged. No Swift change: the outer already accepts a PING once the
    previous one is answered. macOS only (Karabiner lease).

  • Macos: Pass a key that waited behind the main thread untouched
    When the Hammerspoon main thread is held (1.3 s to 5.5 s stalls in the
    issue #100 log, 13.4 s in #99), macOS stops waiting for the tap
    (kCGEventTapDisabledByTimeout) and delivers the key without it, while
    the queued callback still runs afterwards. The physical fence then
    replayed a copy of a key the application already had, and the keymap
    built expansions on it that deleted or retyped the wrong text: doubled
    or swallowed keys around every stall.

    A physical event whose callback is 500 ms later than the freshest
    recent events is no longer consumed, replayed or answered: it passes
    untouched, the keymap restarts its buffer, and the session counters
    record it. Windows (AutoHotkey's own hook thread) and Linux (the evdev
    reader) never replay a delayed physical event, so neither has this path.

  • Macos: Hold a key-up back only behind its own replayed key-down
    A physical event arriving while Ergopti output is still being typed is
    consumed and replayed after it, so typing order holds. A key-up was
    held back the same way even when its key-down had already reached the
    application: rolling a letter into a hotstring terminator kept that
    letter down for the application as long as the expansion took to type,
    long enough for the macOS press-and-hold popup on a plain tap
    (issue #100).

    The fence now holds a key-up only while its own consumed key-down still
    waits unposted in the FIFO; a release types nothing that could overtake
    the expansion. A replayed key-down is still followed by exactly one
    key-up, after it. Windows and Linux do not consume and replay physical
    key events behind their output, so only macOS changes.

  • Macos: Bound every Accessibility call to one second
    Every Accessibility call runs on the Hammerspoon main thread, where the
    keyboard event taps also run, and macOS lets a call to an application
    that does not answer wait six seconds. Issue #100 logged the
    ignored-window probe holding the main thread 1.5 s at an application
    switch, and the 13.4 s freeze of #99 was a chain of such waits; keys
    typed meanwhile came out late, held or lost.

    Boot now sets the process-wide messaging timeout on the system-wide
    element (hs.window.timeout, AXUIElementSetMessagingTimeout) to 1 s
    before the first input owner starts, and logs an ERROR when macOS
    refuses it. A hung application's read fails instead of freezing input.
    Accessibility is the macOS driver's own API, so only macOS changes.

  • Macos: Read the typing application without Accessibility in key taps
    TextSender.focusedAppIdentity and terminalInputTarget run inside the
    keyboard event tap: on every expansion that deletes (terminal pacing)
    and, since 1.0.5, on every bare Return, Cmd+A and Cmd+L. They asked
    hs.window.focusedWindow(), an Accessibility round trip to the focused
    application. An application that does not answer holds it up to six
    seconds while every key of every application waits behind the tap;
    macOS then disables the tap and releases the queued keys late or held,
    which issues #99 and #100 report as swallowed keys and as the
    press-and-hold accent popup on a plain tap.

    The typing application now comes from NSWorkspace, which never asks the
    application. Accessibility is the macOS driver's own API, so only macOS
    changes.

  • Windows: Run the updater stages as bundled -File workers
    Antivirus sandboxes score the Windows updater as a malware stage: it
    published its worker as Base64 fragments in environment variables,
    decoded them, compiled them with [ScriptBlock]::Create and started
    PowerShell with -EncodedCommand. The crash-report minimal writer also
    launched an encoded command. The compiled exe carries that text.

    The staging worker (download, size and SHA-256 checks, swap worker
    publication) is now vendor/ergopti_updater_staging.ps1, started with
    -File and a single -TransportPrefix argument. The release data stays in
    inherited environment values under that prefix: data, never source,
    and out of reach of the cmd.exe wrapper's %VAR% expansion. The swap
    worker is vendor/ergopti_updater_swap.ps1, which the staging worker
    copies next to the staged executable exactly as it wrote the decoded
    text before, and the crash-report fallback is
    vendor/ergopti_crash_fallback_worker.ps1. A missing bundled worker
    fails the install before any launch, with the usual logged error.

    Download, digest verification, replace, restart, rollback and failure
    receipts are unchanged. -ExecutionPolicy Bypass stays on these -File
    launches: it is process scope only, and the default Windows client
    policy (Restricted) refuses every script file without an override.
    macOS and Linux have no PowerShell stage and no equivalent pattern.

    The compiled script loses 3 -EncodedCommand, 1 [ScriptBlock]::Create,
    2 FromBase64String, 1 ExecutionPolicy Bypass and 1 hidden window (the
    swap worker's Start-Process moved to the bundle). Tests now read the
    bundled files, run the real worker through the production argv and
    cmd.exe, and fail if driver code contains an encoded stage again.

  • Config: Keep the Lua table-array cleanup LuaJIT-safe and driver-neutral
    The table-array cleanup compared numbers with math.type, which LuaJIT
    (the Linux daemon and its CI suite) does not have; the LuaJIT gate
    rejected the call. It now reads math.type once into a local that is nil
    under LuaJIT, as the codec owners do.

    The contract case also counted every [hotstrings] row, so the Linux
    driver, which does not read trigger_char, saw two rows instead of the
    one array (CI run 39797045031, Linux ui). It now selects only the array
    rows; whether a driver reads trigger_char is that driver's policy.
    Verified under LuaJIT 2.1 (WSL Debian) and Lua 5.4 (macOS suite).

  • Windows: Show partial typing history after a failed SQLite read
    Any refused SQLite query failed the whole background projection, so the
    dashboard kept its previous snapshot and said nothing about it. The
    maintainer chose to show what was read instead, as macOS and Linux now
    do.

    Inside the projection worker each reader query is now tolerated on its
    own: a refused one contributes no row, every other keeps its exact
    counts, and the blob carries metrics_manifest_incomplete or
    _prefetch_data.incomplete, the flags the shared page turns into its
    incomplete-history notice. Defects other than a refused read stay
    fatal. The worker exits with a new "incomplete" status: the stage is
    delivered to its window but never published as the reusable snapshot
    nor kept in RAM, the reader image is saved only after a complete read,
    and the window arms its bounded full retry, so the next read starts
    over. The resident logs the incomplete read and its recovery once each.
    The Edge fallback, which only reads published snapshots, logs and does
    not open on such a read.

  • Windows: Keep a NUL token or title distinct in dashboard JSON
    The reader built its historical and today n-gram indexes and the window
    title map with json_group_object, which the vendored sqlite3.dll cuts
    at an embedded NUL: a token "a\0b" came back as a second "a" key beside
    the real one and the page kept whichever arrived last. The walker's
    histogram merges were fixed the same way. Every SQL-built reader object
    is now spelled with json_quote over group_concat, and a real in-memory
    SQLite read pins the exact keys of each one.

  • Linux: Show and never keep a partial typing history read
    The Linux dashboard already showed a partial manifest after a refused
    SQLite query, but said nothing about it, read n-grams with no
    completion signal, and kept the first "ready" payload as the cached
    answer whatever its completeness, so a transient failure stayed on
    screen for every later opening of the session.

    The reader now reports n-gram completeness, the payload carries the
    same incomplete flags as macOS for the shared notice, and the bridge
    never keeps an incomplete payload, so the next opening retries a full
    read. The incomplete read and its recovery are logged once each.

  • Macos: Show partial typing history after a failed SQLite read
    Since the completion receipts landed, a transient SQLite error made the
    typing dashboard refuse the whole history: the job asserted, the page
    kept an old snapshot marked as failed, and a range request answered
    null. The maintainer chose to show what was read instead.

    Full, live and range projections now publish their partial counts with
    an incomplete flag that the shared banner turns into a notice. Such a
    read is never saved as the first-paint snapshot and leaves the
    projection cache untouched, so the next open retries a full read. An
    incomplete n-gram extension is merged into a copy for display only.
    The incomplete read and its recovery are logged once each.

  • Config: Offer an unread table array below a read root on macOS and Linux
    The shared Lua cleanup offered whole unread roots and addressable
    records only. A table array under a root a reader consumes
    ([[hotstrings.old_rows]] next to [hotstrings] trigger_char) has no
    addressable record and its root is not unread, so it was never offered.

    With the explicit whole-value capability both drivers already pass,
    each such outermost array is one entry, removed with every generation
    and proven against the typed document: only that path may leave it.

  • Macos: Find webview windows among our own, never every window
    Opening any webview froze Hammerspoon 2.8 s to 3.2 s on macOS 26
    (issue #100). webview:hswindow() and hs.console.hswindow() call
    hs.window.windowForID, an alias of hs.window.get, which runs find()
    over hs.window.allWindows(): every window of every application read
    through Accessibility on the main thread. force_focus paid it on every
    open and up to twenty more times in its retry while a new window was
    still being composited; the focus checks paid it again.

    adapters/own_window.lua makes the native call with windowForID swapped
    for a recorder of the window number, for that synchronous call only,
    then picks the window carrying that number among this process's
    windows, found by PID. Number 0 or a window not listed yet reads as
    nil, so the existing bounded retry still applies; a missing own
    process raises. Every caller (ui_builder focus and focus test, metrics
    dashboards, download window, console placement) goes through it.

    The anti-scan meta test now admits no other hswindow() caller and
    ratchets every global enumeration (allWindows, orderedWindows,
    visibleWindows, get, find, windowForID, hs.window(), window filters);
    the remaining ones answer user gestures or ports, none on an open path.

  • Ci: Re-read a Unit lot whose writer step record lags its job
    Run 39785389458 refused the Common Unit admission of Shared UI
    rendering: the Generators and parity lot was completed and successful,
    but the API still reported its writer step as "pending". GitHub
    publishes a job conclusion before its step records, so the admission
    raced the API, not the lot.

    A refusal whose only cause is a single writer step still pending,
    queued or in progress is now re-read up to six times, 10 s apart. Any
    other writer state still refuses at once, with the same diagnostic,
    and a writer that never completes is refused after the last read.

    The previous commit 90da28c carries this subject by a shared-index
    race but holds another change (test_config_snapshot_obsolete_parent.ahk).

  • Ci: Re-read a Unit lot whose writer step record lags its job
    Run 39785389458 refused the Common Unit admission of Shared UI
    rendering: the Generators and parity lot was completed and successful,
    but the API still reported its writer step as "pending". GitHub
    publishes a job conclusion before its step records, so the admission
    raced the API, not the lot.

    A refusal whose only cause is a single writer step still pending,
    queued or in progress is now re-read up to six times, 10 s apart. Any
    other writer state still refuses at once, with the same diagnostic,
    and a writer that never completes is refused after the last read.

  • Macos: Ask the updater owner whether admission runs packaged
    The instance admission compared the packaged bundle id itself, which the
    one-owner gate refuses: Updater.is_local_source() answers whether this
    is the embedded runtime. Admission now takes that answer, and an
    unanswerable identity is treated as a foreign Hammerspoon, so a refusal
    never quits a process that may not be ours.

  • Windows: Offer bare root keys and unknown table arrays for cleanup
    Boot warned about a bare root key ([].stale_root) and only counted an
    unknown [[x]] table array as "key(s) in skipped sections", while the
    unused-key cleanup skipped section "" and never saw a table array, so
    neither ever reached the offer and both stayed in config.toml forever.

    The semantic snapshot now names each outermost table array once
    (ArrayRows), and TomlConfigTableArrayKind judges it for both the loader
    and the cleanup, so boot names an unknown array and the cleanup offers
    what boot names. A root row is a leaf of the tree root. The document
    writer drops every generation header of a deleted array, which it
    otherwise re-emitted as empty generations, and a listed value covers
    the records and headers beneath its path when its unknown parent
    section is removed. The cleanup page and text name a root row without
    an empty "[]" section.

  • Windows: Own a physical keyboard slot parameter by its publication
    The action-parameter owner judged every binding by a lowercase grammar
    before asking the binding's published catalogue. A physical keyboard
    slot keeps its scan-code spelling (win_SC03A), so its parameter
    (keyboard__win_SC03A__open_url) failed the grammar: boot reported it as
    an unused key, the cleanup offered a live parameter for deletion and the
    scope inventory dropped it.

    A binding its owner publishes as current is now owned whatever its case;
    a retired one stays refused and an unpublished domain keeps the
    lowercase grammar.

  • Launcher: Keep the sole-launcher startup on the main actor
    Splitting applicationDidFinishLaunching behind the instance lock moved
    the Sparkle wiring into a nonisolated method, which the macOS 26 build
    refuses: the user driver, the consent policy and the command router are
    main-actor isolated. The continuation and the lock retry now run on the
    main actor, and the hand-over uses activate(options:), available on the
    macOS 11 deployment target.

  • Launcher: Own the embedded runtime as the only launcher
    LSMultipleInstancesProhibited only covers other user sessions, so
    open -n, a direct exec or an update relaunch racing the old process
    could start a second launcher and a second embedded runtime. The
    launcher now takes a per-user non-blocking flock on a close-on-exec
    descriptor before it seeds MJConfigFile; a held lock is retried for
    5 s (an update relaunch) and then hands over to the running launcher.
    The sole launcher then ends any leftover runtime of the embedded
    bundle identity before starting its own; a Hammerspoon the user
    installed has another identity and is never selected.

  • Macos: Refuse a stray runtime before it arms any producer
    On 2026-10-21 and 2026-10-22 macOS started the embedded runtime beside
    the launcher's own, without the launcher environment. It resolved its
    paths (writing paths.toml into the signed bundle), armed the KC ledger
    timer and path watcher, then aborted behind a blocking fatal dialog
    whose modal run loop kept those producers writing to the shared log.
    The same abort ran os.exit in any Hammerspoon, so a stock Hammerspoon
    pointed at Ergopti was quit.

    Instance admission now runs right after privacy admission, before any
    settings write, timer or path resolution: our own runtime started
    without the launcher exits silently, a foreign Hammerspoon is told why
    the driver did not start and keeps running. Every pre-runtime fatal
    abort also stops the timer scheduler and the KC bridge before its
    dialog.

  • Linux: Chain the n-grams after a correction as Windows and macOS do
    Windows and macOS keep every Backspace in the typing chain: it ends
    "x[BS]" and "yx[BS]", and the next characters continue from it
    ("[BS]x", "y[BS]x", ... up to the heptagrams). The Linux walker pushed
    the sequences ending in a correction but cleared its run after one, so
    "[BS]x" was never counted there and the three dashboards disagreed on
    the same keystrokes. The run now holds the marker; a pause or synthetic
    output still breaks it, and a correction still ends no token longer
    than a trigram.

    A shared corpus pins the exact n-gram tables for four correction
    sequences, one of them across a flush, and replays on all three
    walkers; the Linux test that pinned "[BS]b" at zero now pins the shared
    behaviour.

  • Windows: Extract the exe bundle with tar.exe, not hidden PowerShell
    The compiled exe's first launch expanded its runtime bundle through a
    hidden "powershell -ExecutionPolicy Bypass -Command Expand-Archive"
    child. That is the first thing an antivirus sandbox sees when it
    detonates a downloaded ErgoptiPlus.exe, and an unsigned exe that drops
    a payload through a policy-bypassing hidden script host scores as a
    dropper. A corporate proxy blocked v1.0.4 and later v1.0.5 after such a
    sandbox verdict.

    Windows ships bsdtar as System32\tar.exe since Windows 10 1803; it reads
    the deflated ZIP natively, extracts the v1.0.5 bundle byte for byte in
    about 0.5 s (Expand-Archive took 8-19 s on hosted runners) and starts no
    script host. The absolute path keeps a planted tar.exe out, and paths
    that cannot be quoted for the C runtime argument rules are refused.

  • Windows: Keep a NUL map key distinct when SQLite merges a histogram
    The walker merged every numeric map it owns (n-gram sources, hourly and
    five-minute error buckets, burst lengths) through json_group_object,
    which the vendored sqlite3.dll cuts at an embedded NUL: a key "a\0b"
    came back as "a" and folded into the real "a" key on the second flush.
    The Linux writer had the same defect and was fixed the same way. The
    object is now spelled with json_quote over group_concat, and a real
    in-memory SQLite replay of each of the four merges pins the exact keys.

  • Macos: Name the keyboard group commands in the template call
    c80bd4d moved the keyboard group command table into a local before
    passing it to ManifestMenu.template_rows. The handler bijection credits
    only a literal table of bare local functions at the call, so the five
    Add rows lost their handler (hs 5, baseline 0). The table is literal
    again at both calls, with named restore and clear functions.

    The same commit opened every bindable group with restore, clear and a
    separator but left two suites on the old shape: the declared frame
    order and the row data cases now expect the three scope rows first,
    and skip the separator where they read labels or the disabled flag.

  • Macos: Find the AI prompts' own app by PID, never by name
    The model download prompt and the Hugging Face token prompt activated
    Hammerspoon through hs.application.get("Hammerspoon"), then find(). When
    the name does not match the embedded runtime, both fall back to reading
    every window of every application on the main thread, the freeze that
    cost the lease in issue #99. Both now use RunningApplication.current(),
    and the direct-lookup baseline is empty.

  • Macos: Let the user's own Ollama daemon pull behind a PAC proxy
    Issue #100 (and #99 before it): on a Mac whose network settings use a
    PAC file, enabling the AI with the user's Ollama.app always failed with
    "the system proxy configuration cannot be used by this client" and the
    model was never downloaded.

    ollama pull only asks the running daemon to fetch. A daemon Ergopti
    never starts (Ollama.app, Homebrew, PATH) downloads with its own network
    settings, which the CLI's environment cannot change, so the opaque
    admission refused a fetch it did not govern. A foreign daemon's pull now
    gets the static settings prelude and its own failure is reported; a
    daemon of unknown or Ergopti origin keeps the fail-closed admission.

  • Macos: Read pending lease output before failing an on-time deadline
    Issue #100 lost the Karabiner lease seven times in twenty minutes, each
    PONG or READY deadline firing "0.00 s late", with no worker stderr and a
    worker that exited 0 once Hammerspoon closed its input. Tap-hold keys
    lost their Karabiner rules during every fence and the recovery after it.

    Hammerspoon 1.1.1 reads a streaming hs.task with
    readInBackgroundAndNotify, which delivers only in the default run loop
    mode, while hs.timer and task completions run in every mode. With the
    Ergopti menu open (event tracking mode) the worker's answer sat unread
    in the pipe while its deadline expired on time, and the completion then
    beat the worker's last stderr chunk, which the runner drops.

    An on-time PONG, READY or mode deadline now starts a /bin/cat echo probe
    (adapters/task_output_turn.lua) and judges the worker only once that
    echo is read, so every answer written before it has been delivered. The
    wait is capped at 30 s; STOPPED keeps its direct verdict, its fallback
    revoker reaching the same safe state.

  • Linux: Log a privacy-filter click refused by a metrics transaction
    The Windows driver refused a private-browsing filter click that landed
    on an open configuration transaction, with an error window. On Linux
    the same refusal happens while a metrics scope still owes its rollback,
    but the setter returned false without a word, so the click vanished.
    The three privacy-filter setters now log the refusal; nothing else
    changes, and the state stays as it was.

  • Windows: Keep a late check-row click from toggling the row back
    A menu toggle flipped whatever state it found when it finally ran. A
    click retained during startup committed late, the row still showed its
    old checkmark, and a second click against it waited behind the write,
    crossed the reload and flipped the private-browsing filter back on:
    the row "did nothing" and stayed checked (2026-10-22).

    Check rows now register the predicate that draws their checkmark
    (manifest check and toggle rows, feature rows). A click records the
    state it asks for, the opposite of the checkmark it was made on, and
    that state travels through the write deferral, the startup retention
    and the reload hand-off (replay schema v3). A late command whose row
    already shows that state is skipped and logged instead of run.

  • Ui: Name the unused-settings window with the brand once
    The Windows cleanup window read "ErgoptiPlus — ErgoptiPlus — Réglages
    inutilisés". Its opener passed a label it had already branded as the
    WebView host Title, and Gui_Create brands every caption again. The
    title audit missed it: it resolves the Gui_Create argument, which the
    host reads back from Opts at build time, so it counted the call as an
    unresolved dynamic input. The locale guard only checked translations,
    which were clean.

    The host now names a manifest window from its title_key, so the cleanup
    window passes no caption at all; Title stays for contextual labels. The
    macOS cleanup window opens by app identity through the same manifest
    path, and the Linux GTK window composes its caption through one owner,
    app_caption.

    Guards: on each driver, (window-title-brand-once) builds every manifest
    window through its real caption path in all 21 locales and refuses a
    second brand or a repeated segment; on Windows it also opens the real
    cleanup window, which reproduced the doubled caption before the fix.
    The audit now follows Map("Title", ...) and Opts["Title"] := writers.

  • Windows: Defer a tray click behind an in-flight config transition
    A scoped configuration commit (ConfigScopeCommitOperations) holds the
    terminal bundle across its durable I/O and until its reload launches.
    AutoHotkey let the tray open over that thread, and the clicked row ran
    on top of it. The menu deferral treated any terminal bundle as lasting
    until exit, so it ran the command at once and the commit was refused:
    "another configuration transaction is already in progress", with an
    error window (private-browsing filter, 2026-10-22).

    MenuCommandRun now waits on MenuCommandConfigBusy, which also counts an
    active bundle while no reload successor is pending and no recovery
    barrier is retained. Once the successor launches, the existing reload
    hand-off takes the command; a retained barrier is still not awaited.

  • Menu: Open the tap-hold key delay prompt from its own row
    The per-key tap delay of every tap-hold key was a submenu whose only
    row opened the delay prompt, and a row that opens a submenu is never
    clicked. The row that shows the current delay is now the command
    itself on Windows, macOS and Linux, with the same caption and the same
    prompt and setter behind it. The macOS "use global delay" check, the
    submenu's second row there, moves up next to it, so no tap-hold key
    keeps a nested delay menu. The retired row's label leaves the 21
    locales. The renderer caption-group contracts used that submenu as
    their fixture and now use a group each driver still draws.

  • Windows: Stop Win+S cancelling its own selection read
    copy_selected_path (Win+S) did nothing. GetSelectionAsync cancelled the
    capture as "superseded by input" when A_TimeIdlePhysical moved more
    than 20 ms after the shortcut, but releasing S and then Win moves it
    too, so whenever the application copied more slowly than the key
    release the copied selection was dropped, silently. The keylogger
    ledger shows the copies arriving and nothing opening.

    The capture now takes KS_GetPhysicalInputGeneration as its ticket: the
    dispatcher counts physical key-downs, mouse-downs and wheels, never
    releases, and its InputHook callback runs before the hotkey thread, so
    only a later action cancels. The copy chord goes out at
    TEXT_SENDER_SEND_LEVEL: from the #InputLevel 2 Win broker it was sent
    at level 2, where the driver's own I1 InputHooks took it for typing
    (the keylogger logged a "Win+C") and it would advance that ticket.

    Every ending without a selection, and an empty selection, is now
    logged with its reason and foreground process. An empty selection
    opens nothing, as on Linux and macOS.

  • Windows: Name the clipboard holder when a selection read fails
    At 08:30:32 Win+S (copy_selected_path) failed because another process
    kept the clipboard open past ClipboardAll's own one-second wait (a
    600 ms foreign hold succeeds after ~440 ms, a 1.6 s hold fails at
    1000 ms). GetSelectionAsync turned that external condition into an
    ERROR, which opened the generic error window, and no log said who held
    the lock, so the occurrence could not be diagnosed.

    CB_DescribeClipboardHolder reads GetOpenClipboardWindow (process, pid,
    whether it is this driver) and GetClipboardOwner; CB_SaveAll logs it
    with every failure. A failed selection snapshot now logs a warning with
    that holder and shows a notice naming it (selection.clipboard_busy in
    every locale) instead of raising an error. No further retry: the
    snapshot already waited the bound CB_OpenClipboardWithin uses. macOS
    and Linux have no clipboard lock, so they have no equivalent path.

Performance

  • Toml: Build the expected model from the one source parse
    TOML_BuildConfigDocumentCandidate parsed the same source twice: once
    for the source model and again for the expected model it then mutates.
    The semantic setter mutates tables only and replaces values whole, so
    the expected model is now a table copy of the first parse.

    A changed document-mode build drops from 3 to 2 parses, an unchanged
    one from 2 to 1, and a real write from 4 to 3 (the schema journal keeps
    its own independent admission parse). The post-ready full save is an
    unchanged-source build, so each start saves at least one full parse
    (about 110 ms on the maintainer's 830-line file). A parse counter pins
    the cost per transaction.

  • Changelog: Switch Versions tabs locally over the fetched list
    Every host fetches the same GitHub release list whatever the channel,
    and the page filters it by the shared registry. A tab still posted a
    fetch, so each switch paid a 1.0 to 1.7 s download of about 1.7 MB,
    the Windows escape of that text on the hook thread, a re-parse and a
    spinner, for a list the page already held.

    The page now keeps the last complete list and a tab filters it locally:
    no bridge message, no spinner, about 15 ms to paint. A switch still asks
    the host while a load is in flight, after the Linux interim answer that
    carries one cached release, and after an error, when the hosts drop the
    list an install is looked up in, so failures stay visible. Retry always
    reaches the host. This covers Windows, macOS and Linux, which share the
    page and fetch the same list.

  • Windows: Escape JSON string literals in native passes
    The Versions window hands its page the GitHub release list as one JSON
    string literal, about 1.7 MB of release notes. JsonStringLiteral walked
    it one code unit at a time in script, which took 2.7 to 3.3 s on the
    thread that also runs the keyboard hook, on every open and on every tab
    switch.

    Escape through StrReplace passes and walk only the rare C0 units that
    have no short escape. StrReplace stops at the first NUL, so the text is
    split on NUL first and each run escaped on its own. Output is identical
    on the release list, the Atom feed and every code unit; the same list
    now takes 66 to 115 ms.

Refactoring

  • Windows: Draw the personal submenus as renderer template children
    The default-section, file and folder submenus of the personal hotstrings
    were the last three rows Windows driver code drew itself: each created a
    Menu and appended its declared frame into it, and the default-section
    callbacks then checked, unchecked and renamed rows of that Menu by label.

    The renderer now owns these children. MenuRenderer_TemplateChild reserves
    one into the caller's release ledger (before the commands that repaint it
    exist), MenuRenderer_FillTemplateChild draws the admitted template once,
    and the declared parent rows (group, personal file, personal folder) take
    the handle and attach the child only while it still holds exactly the
    rows the renderer drew. MenuRenderer_RepaintTemplateChild moves the tick
    and renames the parent row by position, with literal ampersands escaped,
    and refuses before the preference write once the child was released or
    is not attached to the given parent. A callback retained past a rebuild
    used to write the preference and then repaint a deleted menu.

    Captions, order, unavailable reasons and child commands are unchanged.
    The native menu row census drops from 3 to 0 on Windows.

Documentation

  • Todo: Keep the CI follow-ups from the October 23 green run

  • Todo: Keep the luajit command on one wrapped line
    dacee69 split the luajit ergopti_hotstrings.lua code span across
    two lines of a list item, which Prettier rewrites, so the formatting
    check of run 39871833938 failed. The span now starts its own line.

  • Todo: Record the Linux file-manager, window-list and instance-lock results

  • Todo: Mark menu clicks during a reload done on macOS and Linux

  • Todo: Mark the last three native Windows menu rows done

  • Todo: Keep the October 22 agent findings and the open decisions
    Record the keystats rename plan, the 1.0.6 order, the macOS input follow-ups, the
    remaining separator, metrics, dashboard, config, Linux and CI items reported by
    the agents, the commit-message hygiene list, and three new maintainer questions.

  • Skills: Read the Paris clock with Get-Date, not TZ in Git Bash
    Git Bash on this Windows host has no tzdata, so TZ=Europe/Paris date silently
    prints UTC; a schedule given to the maintainer was two hours off.

  • Macos: State what the event clock assumes about stamp units
    CGEventGetTimestamp is documented as nanoseconds since startup, yet may
    hand over raw Mach ticks on Apple silicon; the adapter tries both scales,
    and its header no longer states either as a fact.

  • Architecture: Regenerate the diagram with the event clock adapter
    npm run gen:diagram after adapters/event_clock.lua was added.

  • Todo-agents: Hand off the key tap fixes for issues #99 and #100
    Lists the local commits behind the swallowed keys, the accent popup on
    a plain tap and the lease keepalive, and what stays open: the
    karabiner.json redeploy per recovery, the per-keystroke AXValue read and
    stale events in the taps other than the keymap.

  • Todo: Hand over the Windows and Linux hold action picker
    The handoff listed the Windows and Linux hold as kept on purpose until the
    maintainer decided; record what landed in 7eaf277 and the manual checks
    on those two systems.

  • Memory: Record the key tap invariants behind issues #99 and #100
    The physical fence pairs key-ups with their own replayed key-down and
    never replays a stale event, key taps never ask Accessibility and every
    AX call is bounded, the lease keeps its worker fed while answers are
    unread, and the session key counters are the first lines to read in a
    report of swallowed keys or an unasked accent popup.

  • Architecture: Regenerate the diagram with the own window adapter
    0ab0bd4 added adapters/own_window.lua without regenerating
    architecture.md, so the diagram sync check failed on run 39797045031.
    Regenerated with npm run gen:diagram.

  • Todo-agents: Hand off the v1.0.5 proxy verdict and SignPath application
    v1.0.5 passed a corporate proxy once and was blocked two hours later on
    another PC, while v1.0.2 still passes. The static comparison of the
    three released exes shows identical PE images apart from resources and
    no pattern in v1.0.5 that v1.0.2 lacks, so the verdict follows a new
    unsigned hash through sandbox and reputation scoring. The handoff
    records the hashes, the comparison, the Microsoft false-positive text
    and what remains; the new SignPath file holds the eligibility table,
    the form answers and the switch-on steps for the maintainer. Memory
    keeps the lesson that the compiled script is scanned as plain text.

  • Memory: Record that macOS starts the embedded runtime alone
    Two reports show a second embedded runtime started by macOS beside the
    launcher's own; the entry routes the next reader to the admission gate
    and the launcher lock instead of hunting for an Ergopti launch path.

  • Readme: Publish the code signing policy and privacy statement
    SignPath Foundation signs only projects whose home page states a
    "Code signing policy" with the team roles and a privacy statement. The
    section names what is signed (only the CI-built ErgoptiPlus.exe), who
    approves each request, and every network request the application
    makes, with the typing metrics off by default and kept local. The
    install notes also state the Windows 10 1803 floor that the bundle
    extraction through tar.exe sets.

  • Memory: Note the Windows walker shares the NUL map key trap

  • Todo: Record the maintainer's second batch of October 22 answers
    The site deploy design is confirmed (main to ergopti.fr, dev to
    ergopti.fr/dev/) and its workflow definition being read from main is
    accepted; the macOS dashboard shows a partial history's counts with an
    incomplete-history mention; the Versions window hands the newest feed
    release to Sparkle; the per-row number-row switches do not cover the
    October 13 request, which becomes big item 4, a per-layer layout
    composer. The WPAD question is closed: the maintainer's Mac uses only a
    corporate PAC file, so resolving that PAC for an Ergopti-started Ollama
    becomes a checklist item.

  • Todo: Record the maintainer's answers of October 22
    Issue #100 keeps 2353a72 (a foreign Ollama daemon pulls through a
    PAC proxy); the macOS webview-open freeze is fixed now; SignPath
    Foundation is applied for now; the Windows and Linux tap-hold hold rows
    open the shared action picker with holdable actions only. The four
    questions move to answered and their work joins Wave 2 as in progress.

  • Todo: Record the ISO key left of 1 as confirmed on the Mac
    The maintainer confirmed on 2026-10-22 that an action assigned to the
    key left of "1" now lands on that key, not on the ISO 105th key, so
    the Karabiner-EventViewer check and its revert option for 356d14a
    are gone.

  • Todo: Shrink the checklist to three big items and checklists
    v1.0.5 is released (17d4a3f), so the v1.0.4 header, the October 21
    state, handoff table, dev-red notes and the after-release plan are
    history. Finished items are removed after checking each against Git and
    the code: exe metadata (965ff75), timers and tap-binding tests
    (d5c520c, e9a50f5), the updater freeze (8a43bd8), the Codex
    branch integration, the Report-a-bug locale wording (all 21 locales
    already carry it) and the empty app-lookup baseline (e57f4cc).

    What remains is three big items (Windows corporate networks and trust,
    macOS Karabiner input reliability, Windows input hooks), a Wave 2 list
    with the work agents are doing now, a one-line backlog, the maintainer
    checks (post-release actions, Windows PC, Mac, Linux), and one list of
    CI-only qualification receipts. Pending decisions move to the questions
    file, with two new ones from October 22 (issue #100 PAC pulls through a
    foreign Ollama, the webview window lookup freeze).

    The ten finished handoff files this replaces were deleted from the
    shared index and landed by accident inside 18ca214; their open lines
    and physical checks are in the checklist from this commit on.

  • Memory: Keep the catalogue tray-click and backspace limits
    The retired AppImage catalogue and backspace handoff files held two
    durable lessons: the catalogue's right click closes the menu its left
    click opened, so only a status window reaches the screenshot; and the
    word-start rules have two known limits (browser inline autocompletion,
    Linux Shift+Enter in a terminal TUI) that are not bugs.

  • Memory: Record the NUL-bounded StrReplace and brandless captions
    StrReplace ends its haystack at the first NUL while RegExMatch and
    SubStr do not, which JsonStringLiteral now works around. The window
    composer is the only place that adds the product name, so a branded
    caller label or locale caption shows it twice.

  • Architecture: Regenerate the diagram with the task output turn
    25411ee added adapters/task_output_turn.lua without regenerating
    architecture.md, so the diagram sync check failed. Regenerated with
    npm run gen:diagram.

  • Todo: Hand off the keyboard group restore and clear rows
    The handoff names where each driver draws the two rows, what the
    narrowed Shortcuts scope restores and clears, the tests and the manual
    check left.

Tests

  • Windows: Read the loading request in the Critical that publishes it
    The placeholder-loading case of the LLM nav event owner read
    _TooltipPendingRequest on the line after LLM_TooltipShowLoading. The
    tooltip render is debounced by 1 ms, so on a slow runner its timer ran
    between those two lines, consumed the request and the assertion read 0
    (run 39887661465; the same commit passed on 39882515538). The case now
    publishes and reads the request and serial in one Critical section, and
    asserts on what it read.

  • Windows: Read a quoted table header in the installed profile
    Run 39878950422 refused the compiled upgrade before launch with
    "Unsupported installed section layout": the prior dev release now saves
    each extension module under a quoted key segment, such as
    [hotstrings.modules."ext:ergopti-demo:demo-phrases"], and the
    installed-profile reader of the upgrade contract admitted only bare
    dotted headers. It now admits bare and basic-quoted key segments, and
    the installed-user fixture carries such a table.

  • Ci: Mutate the 20-minute audio budget in the wiring self-check
    ce3f736 moved the audio install pin to 20 minutes but left the
    self-check mutating "timeout-minutes: 10", which no longer occurs, so
    the mutation changed nothing and the rule reported it could not fail
    (run 39875037375). The self-check now mutates the real 20.

  • Ci: Pin the virtual audio install budget at 20 minutes
    74b5fe5 raised the install-only audio step to 20 minutes, but the
    pipeline wiring contract still required 10, so it refused the step on
    run 39873463783.

  • Linux: Remember the colour mode away from the shipped default
    fa965e4 aligned the Linux recommendations on Windows and shipped
    metrics.wpm_widget_colors as false. The widget stores only a choice that
    differs from what ships, so the case that chose false now chose the
    default, left no entry and failed. It now chooses the opposite of the
    manifest default, whichever it is, and also checks that choosing the
    shipped mode again drops the entry.

  • Windows: Load the partial read owner in the apps DOM child
    0010418 made the metrics projection call KLR_BeginPartialRead and
    KLR_ProjectionQuery from the new keylogger_reader_partial.ahk. The apps
    DOM integration builds its child from an explicit list of owner files,
    which did not name that one, so every projection worker died on an
    UnsetError at keylogger_prefetch.ahk:1022 and the child exited 1. The
    first line of its output, a long-standing LocalSameAsGlobal warning in
    vendor/WebView2.ahk, hid the cause in the CI summary. The owner is now
    in the list and the owner count is 44.

  • Linux: Bind the picker in the hidden tap row case
    7eaf277 made the hold row of a key open the shared action picker, and
    the "hides declared tap row" case clicks it and confirms through
    picked, but its callback still named that parameter _, so it read
    an undefined global and failed on every run. The parameter is bound.

  • Macos: Pin the hold picker contexts shared with Linux
    The cross-platform refusal case used tap_hold_hold_action as its example
    of a macOS-only context, and 7eaf277 declared it for Linux too, on
    purpose: the Linux hold opens the shared picker. The refusal now uses
    key_combination_tap_action, still macOS-only, and the case pins that the
    hold context is Linux's and not Windows'.

  • Macos: Admit the stale verdict after the one keyDown classification
    The audit-v5 and audit-v6 guards pinned the exact onKeyDownRaw call to
    (e, provenance, provenance_status). The wrapper now also passes whether
    the key waited behind the main thread, a verdict derived after its one
    fenced classification; the guards still require that classification to
    feed the only raw route, and accept further arguments after it.

  • Macos: Pin that a lease fence leaves no key down
    Issue #100 lost the Karabiner lease eight times in an hour, each fence
    switching every Ergopti rule off while keys may be held. A held output
    key is what macOS repeats or turns into the press-and-hold popup.

    The generated tap-hold graph is replayed through the Karabiner model
    for every tap-hold key and hold kind, fenced before the threshold,
    after the hold went down and before the press: once every physical key
    is up, no output key, modifier or layer stays on.

  • Windows: Seed the NUL map key in SQL, where a Map cannot hold it
    The NUL map key regression built its stored map from an AutoHotkey Map,
    whose string keys end at the first NUL: "a\0b" and "a" were one key, the
    encoded batch never held the NUL key and the four cases failed against
    the fixed merge as well. The stored column now takes the NUL key from
    SQL, as another writer's row can hold it, and a walker delta merges into
    it through the vendored DLL. Against the json_group_object merge the
    same data yields "61|20,61|1", a duplicated "a" key; the json_quote
    merge keeps "61|20,610062|1".

  • Macos: Match the foreign daemon pull count in the receipt line
    eb6de67 raised the pull activation assertion to 17 but left the
    printed-receipt match at 16, so the managed Ollama protocol check
    still refused the 17-control run. Both now name the same count.

  • Hs274: Run the producer tooling tests in the shared lane
    Deleting the hs274-*.yml dispatch workflows left the HS-274 Python
    suites and the portable C++ tests run by no workflow, while the owned
    runtime build pins the producer patch set and stream headers they
    cover. A regression there would only surface in the native build.

    The runner discovers every hs274_*_test.py, names why each excluded
    suite runs elsewhere, fails on a stale exclusion or a missing
    interpreter or compiler, and compiles the two C++ tests that need no
    vendor headers. It runs on the Linux shared lane only, where python3
    and c++ are present.

  • Macos: Count the foreign daemon pull activation control
    2353a72 added the (ollama-foreign-daemon-pull) case to the pull
    activation suite, which now passes 17 controls, while the protocol
    harness still required exactly 16.

  • Macos: Read arm_ack_timer through its forward declaration
    25411ee forward-declared arm_ack_timer so it and the output-turn
    probe can call each other, which the hardening-d contract no longer
    found ("arm_ack_timer was not found"). It now also reads the
    arm_ack_timer = function( definition and still checks the three
    deadlines that function picks.

  • Windows: Read the replay command from a parameter, not the loop
    af404db/fb877dbd51 refused invalid requested states with a closure
    over the for variable inside AssertThrows. AutoHotkey 2.0 rebinds that
    variable away from the captured cell, so each case threw UnsetError and
    passed whatever the decoder did. A helper now builds the document from
    its parameter, and the three refusals are real.

  • Linux: Find the tap-hold delay row one level up
    0d5bd45 made the row that shows a key's delay the command itself,
    with no nested delay submenu, but the prompt-cancel cases still looked
    for the zenity --entry leaf two levels down and found no row. They now
    search the per-key submenu's own leaves.

  • Menus: Authenticate the native menu routes through the checkmark ABI
    fb877db gave RegisterMenuItem a CheckedFn parameter and a guard, and
    let _MR_AppendTemplateRowsAdmitted admit a checked_fn field. The native
    startup binding and the shortcut construction proof pin those exact
    bodies, so both stopped crediting their sections: the cold command,
    inert and shortcut result frames lost their reader and the manifest
    readers, parity and category coverage gates went red.

    The pins now name the new reviewed bodies. New mutation controls refuse
    a required or nonzero CheckedFn default and a removed checkmark guard,
    so the three-argument cold caller stays authenticated.

  • I18n: Refuse a caption translation that carries the brand
    menu.llm.api_dialog_title read "ErgoptiPlus — API endpoint" in all 21
    locales, and both callers hand it to a brand-adding composer: the
    Windows API test verdict goes through Ui_MsgBox and the Linux entry
    prompts through the zenity title composer, so both showed the brand
    twice. The audit could not resolve it because the key reaches the
    dialog through a helper parameter. The translations themselves were
    corrected in 0d5bd45, which picked them up from the shared tree.

    The title audit now scans the translations of every caption key (each
    manifest app and presentation title, and every *window_title or
    *dialog_title key) and refuses one that already starts with the brand;
    on the old translations it lists the 21 API-entry captions.

  • Windows: Keep the chord-release case off the runner's clipboard
    On CI the production restore of the hand-driven poll stays in retry
    when the runner cannot open the clipboard, so asserting that it settled
    failed there while the cancellation reason it checks was right. Accept
    the job's own retained debt, and hold Critical while stopping the armed
    poll timer: under load it fired before the test could stop it.

Chores

  • Release: Publish v1.0.6
    Release Ergopti+ 1.0.6 from dev 996013a; full run 39889203911 at e0dd662
    passed on all lanes and only the TODO changed after it.

    macOS: swallowed keys and accent popups on a tap (no Accessibility call in
    the event tap, key-ups never held behind an expansion, late keys pass
    through), Karabiner lease kept alive while a menu is open, webview windows
    open without a 3 s freeze, AI off is inert, Hammerspoon coexistence.
    All systems: Win+S opens what the selection names, instant Versions tabs,
    reset and clear rows per shortcut group, tap delay edited from its row,
    shared action picker for holds, partial typing history shown, menu clicks
    during a reload replayed. Windows: updater stages run as bundled -File
    scripts, no encoded PowerShell, first launch extracts with tar.exe.

    Release-As: 1.0.6

  • Hs274: Retire the complete fork candidate and two dead probes
    ADR 011 decision 1 ships a headless three-product runtime; the
    karabiner_candidate tooling rebuilt all ten Karabiner-Elements products
    and installed them over the official application, which that decision
    rules out, and no workflow or build script uses it since the
    hs274-producer-build.yml deletion. Its package-identity fixture goes
    with it.

    hs274-native.swift and hs274-hid-device.c were one-shot capability
    probes of the deleted hs274-native.yml; nothing reads their receipts and
    their findings (Quartz fields cannot tell a remapped key from a physical
    one, root lacks the IOHIDUserDevice entitlement) stand in the ADR
    alternatives and in macOS memory.

    The rest of tools/diagnostics/hs274* stays: the owned runtime build
    pins the producer patches, stream headers and hs274_native_build.py by
    hash, macOS tests load the context and persistence helpers, and
    hs274-remap.py with hs274-core.py and hs274-provider.py is the native
    fixture acceptance builds on. ADR 011 now records that ownership.

Other

  • Linux: Install the window-switch prerequisites in their own step
    The cursor-display window switching step downloaded its packages, fonts
    included, inside its 4-minute test budget. Runs 39862893510 and
    39876412279 both timed out in the download, and the killed apt-get then
    held the dpkg lock for every later install of the input-display row.
    The packages now install in "Install window-switch prerequisites" (20
    minutes); the test keeps its 4 minutes and the registration contract
    refuses an apt install back in it. The shard and scoped-branch
    inventories list the new step.

  • Linux: Give the virtual audio install step a 20-minute budget
    pulseaudio depends on libasound2-plugins, which brings about 40 MB of
    codec libraries. On run 39862893510 the Azure mirror served them at
    about 60 kB/s, steadily rather than stalled, so the 10-minute install
    step timed out; its killed apt-get then held the dpkg lock and every
    later install of the desktop-webview row failed with exit 100. The step
    only installs, as the WebKit one does, and gets the same 20 minutes.

  • Windows: Reseal the updater source pins after the -File workers
    self_update.ahk is the only pinned source that changed since the last
    seal: the staging transport now runs the bundled staging worker with
    -File and a transport prefix, and no longer builds its worker and swap
    scripts as encoded text. The updater cohorts pass on that source.

  • Windows: Forbid encoded PowerShell stages in the compiled exe
    The updater and the crash-report fallback no longer launch encoded
    commands, so the antivirus surface gate forbids -EncodedCommand,
    [ScriptBlock]::Create and FromBase64String in the compiled script and
    -EncodedCommand in the bundle, instead of holding them at a ceiling.
    The measured ceilings drop to ExecutionPolicy Bypass 17 and hidden
    windows 3; the bundle gains measured ceilings for hidden windows (3)
    and ScriptBlock creation (1, the Ollama model worker importing its
    verified local functions). A local build of the previous source fails
    the new policy on all five counts.

  • Windows: Reseal the updater source pins after the config writer changes
    toml_document.ahk is the only pinned source that changed since the last
    seal: the document writer now drops every generation header of a
    deleted table array, builds its expected model from the one source
    parse and counts its parses. The three updater cohorts pass on that
    source.

  • Linux: Install the model pull window packages in their own step
    The network-ai row fetched 43 MB of GTK, WebKit and lgi packages inside
    the model pull test step, whose budget is 2 minutes. On run 39776223751
    the Azure mirror took longer than that and the step timed out before a
    single assertion ran, which says nothing about the code. As for the
    desktop-webview row, the packages now install in a separate step with a
    20-minute budget, and the test keeps its own 2 minutes. The shard and
    scoped-branch inventories list the new step.

  • Windows: Sign stable releases through SignPath once configured
    Reputation-based engines block a new unsigned exe until its hash has
    history, which is why v1.0.5 passed a corporate proxy once and was
    blocked two hours later on another PC. Every release resets that
    history; an Authenticode publisher keeps it across releases. Modern
    code-signing keys live in an HSM, so the existing PFX path cannot carry
    a public certificate any more.

    The SignPath Foundation flow is wired and stays off until the
    maintainer sets the repository variables SIGNPATH_ORGANIZATION_ID,
    SIGNPATH_PROJECT_SLUG and SIGNPATH_SIGNING_POLICY_SLUG and the secret
    SIGNPATH_API_TOKEN. A stable release then uploads the unsigned exe,
    submits it with the tracked .signpath artifact configuration through
    the SignPath action pinned to its v3.0 commit, waits for the manual
    approval, and replaces the exe only after its signature is Valid and
    its publisher equals WINDOWS_SIGNER_SUBJECT. A partial configuration,
    or SignPath and a PFX together, fails the step. Dev prereleases stay
    unsigned, since each request waits for a human approval. The release
    job counts SignPath as configured signing, so the unsigned stable
    exception no longer applies once it is on.

  • Windows: Gate the antivirus surface of the compiled exe
    ErgoptiPlus.exe embeds its AutoHotkey script as plain text, so proxy
    and antivirus engines read every PowerShell stage in it. v1.0.4 was
    blocked after an inline Add-Type/DllImport clipboard stage reached that
    script, and nothing in CI would have noticed it coming back.

    The Windows build now parses the compiled exe's PE resources, reads the
    script and the deflated runtime bundle, and checks them against
    tools/ci/windows-exe-av-surface.json: inline C# P/Invoke, PowerShell
    screen capture, Expand-Archive, Invoke-Expression, Run keys, scheduled
    tasks and process injection are forbidden; the remaining PowerShell
    launch patterns (ExecutionPolicy Bypass, -EncodedCommand, hidden
    windows, base64 decoding, ScriptBlock creation, RunAs) cannot exceed
    their measured counts. The keylogger wording is reported on every build
    so its rename can be measured. The released v1.0.4 exe fails the gate.

  • Windows: Reseal the updater source pins after the keyboard group rows
    c80bd4d changed infra/config_io.ahk and tests/run_all.ahk, two of
    the 21 sealed sources, without resealing, so the full run 39767542845
    refused the stale pins. The independent label oracle and the frozen
    child diagnostics are unchanged and npm run
    test:windows-updater-original-source passes. Sealed against e57f4cc
    with the repository sealer.



Generated by CI

Windows signature qualification

WINDOWS AUTHENTICODE UNSIGNED / qualified:false (source 950979d): ErgoptiPlus.exe has no Windows code-signing certificate for this v1.0.6 release. All tests, build, package, source and asset integrity, and installation checks remain mandatory. Windows may show an unknown-publisher warning. Signature evidence. Future signing requires SignPath Foundation enrollment and qualified GitHub integration.