f39230e8f49d53151a67a0cf7e78a4da83856f6f
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9c8fa9340c |
refactor: drop milestone names + consolidate clients; loss-recovery & rumble fixes
apple / swift (push) Failing after 40s
audit / cargo-audit (push) Failing after 1m12s
windows-msix / package (push) Successful in 1m37s
windows / build (push) Successful in 1m14s
android / android (push) Successful in 4m48s
ci / web (push) Successful in 27s
ci / rust (push) Successful in 4m21s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 19s
deb / build-publish (push) Successful in 6m3s
flatpak / build-publish (push) Successful in 4m13s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m16s
docker / deploy-docs (push) Successful in 18s
Two bodies of work in one commit (the rename moved files the fixes also touched). Naming/structure cleanup (pre-launch): - Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host, m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source-> Punktfunk1Options/Punktfunk1Source. - Clients consolidated out of crates/ into clients/: punktfunk-client-rs-> clients/probe (crate punktfunk-probe), client-linux->clients/linux, client-windows->clients/windows, punktfunk-android->clients/android/native (crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI contract is unchanged). crates/ now holds only core + host. - Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site, kept only in docs/implementation-plan.md. docs/m2-plan.md-> docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated. Client loss-recovery (video froze and never recovered after a brief drop): - Export punktfunk_connection_frames_dropped through the C ABI (the core already tracked it for the client keyframe-recovery loop; it was never reachable from the ABI clients). Regenerated punktfunk_core.h. - Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll frames_dropped and request a keyframe when it climbs -- the same loss-driven recovery Linux/Windows already had. Under infinite GOP the decoder silently conceals reference-missing frames, so the decode-error trigger rarely fires. Apple rumble robustness (worked then went spotty -- DualSense + Xbox): - Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio interruption / server reset) and drop the permanent `broken` latch on a transient drive failure; latch only when the controller truly has no haptics. - Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging. Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift. Not runnable on this box (verify in CI): Gitea workflows, gradle/Android, flatpak, Swift/decky. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a0f6cddc70 |
feat(host/windows): WGC helper subcommand (two-process secure-desktop, step 3)
`m3-host wgc-helper --target-id N --gdi NAME --mode WxHxHz --bitrate K`: the USER-session half of the two-process secure-desktop design (docs/windows-secure-desktop.md). Opens WGC on the EXISTING SudoVDA output by GDI name only (never creates a virtual output — a second topology owner re-trips the ACCESS_LOST born-lost storm), encodes via NVENC, and ships framed Annex-B AUs on stdout for the SYSTEM host to relay onto the live QUIC session: `[u32 magic "PFAU"][u32 len][u64 pts_ns][u8 keyframe][data]`. tracing → stderr so stdout stays the pure AU stream. cfg-gated windows-only; Linux build unaffected. scripts/headless/win-build.cmd: the canonical box build script (sets PUNKTFUNK_BUILD_VERSION so build.rs stamps the version + the NVENC LIB path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5bc257f1ae |
fix(headless/kde): virtual Punktfunk speaker + restart host with the session
ci / web (push) Successful in 27s
ci / rust (push) Successful in 2m7s
apple / swift (push) Successful in 1m14s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 1m36s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m19s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m50s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m25s
docker / deploy-docs (push) Successful in 18s
Audio: a headless host has no speakers, and on a LAN with AirPlay devices PipeWire picks a random HomePod as default — so desktop audio (which the host captures from the default sink's monitor) went to a HomePod over AirPlay instead of to the client, and there was no "Punktfunk" output to select. Ship a `punktfunk-sink.conf` (a `support.null-audio-sink` adapter — NOT the non-existent module-null-sink, which makes pipewire refuse to start) with high priority.session so it's the default; run-headless-kde.sh installs it and restarts pipewire once on first install. The host then captures its monitor and streams it. (Disable AirPlay sinks out of band: `dnf remove pipewire-config-raop`.) Input: the host's libei portal D-Bus connection goes stale when the compositor session restarts the portal under it, and the in-process reopen loop can't recover it (EIS setup keeps timing out) — only a full restart does. Add PartOf=punktfunk-kde-session.service so the host restarts with the session. Both verified live on the Fedora 44 KDE box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d78bbdffe2 |
fix(headless/kde): start Xwayland + detect its display so X11 apps work
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 31s
apple / swift (push) Successful in 1m19s
ci / rust (push) Successful in 2m4s
ci / bench (push) Successful in 1m41s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m21s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m49s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m27s
X11/Electron apps (Discord — "Missing X Server or $DISPLAY", Steam, many launchers) failed in the headless KWin session: `kwin_wayland --virtual` starts NO X server unless asked, and even with one KWin reserves the X11 display + starts Xwayland *on demand* (no Xwayland process or "Using public X11 display" log line until the first client connects) — so the old detection (pgrep the Xwayland process) found nothing and never exported DISPLAY. Two fixes: pass `--xwayland`, and detect the display from the reserved /tmp/.X11-unix/X<N> socket (with the log + process checks as fallbacks). Verified live on the Fedora 44 KDE box: DISPLAY=:0 lands in plasmashell + the activation env and xdpyinfo responds, so menu-launched X11 apps open a display. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e4b10f057a |
fix(headless/kde): make libei input work headlessly — portal + pre-seeded RemoteDesktop grant
ci / web (push) Successful in 27s
ci / bench (push) Successful in 1m41s
ci / docs-site (push) Successful in 31s
apple / swift (push) Successful in 1m31s
ci / rust (push) Successful in 2m5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m17s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m56s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m25s
On a headless KDE appliance, libei input injection silently failed: the EIS socket comes from the xdg RemoteDesktop portal, which never came up, and even up it would pop an unanswerable "Allow remote control?" dialog. Three fixes in run-headless-kde.sh, all idempotent + safe on the dev box: - Reach graphical-session.target: xdg-desktop-portal is ordered behind it and its start job fails without it, but a headless linger session never gets there and Fedora's target has RefuseManualStart=yes — drop that in once, then start the target. - Start the portal with `start` (the old `try-restart` is a no-op when inactive — the first-boot case), so it actually comes up. - Pre-seed the RemoteDesktop grant: vendor the `kde-authorized` permission-store GVariant DB and copy it to ~/.local/share/flatpak/db/ (never clobbering an existing one), so the portal grants RemoteDesktop without a dialog. Shipped by the RPM + .deb. Diagnosed + fixed live on the Fedora 44 KDE box: libei devices RESUME and emit (MouseMove/keys). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
268733f968 |
fix(headless/kde): find the probe binary on PATH for packaged installs
apple / swift (push) Successful in 1m16s
ci / rust (push) Successful in 1m25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 30s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 3m2s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m54s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m19s
run-headless-kde.sh gated KWin readiness on `$ROOT/target/release/punktfunk-host probe-compositor`, else `cargo run`. On an RPM/.deb install ROOT resolves to /usr/share (no target/ tree) and there's no Cargo.toml either, so the probe could never succeed: the session unit hit its 30s readiness timeout, exited, and systemd restart-looped it forever — KWin never reached the plasmashell step, so the streamed virtual output was an empty black desktop. Add a `command -v punktfunk-host` branch (the packaged /usr/bin binary) between the source-tree and cargo-run fallbacks. Verified live on the Fedora 44 KDE host: session goes stable (NRestarts 0), plasmashell comes up, and a client streams the real desktop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e1242546f2 |
fix(headless-kde): don't let set -e abort the session when Xwayland isn't up yet
ci / rust (push) Has been cancelled
The Xwayland-DISPLAY poll did `d=$(pgrep -a Xwayland | grep … | head -1)`, but under `set -euo pipefail` pgrep/grep exit non-zero when Xwayland isn't running, so the command substitution failed and `set -e` aborted the WHOLE script — killing KWin with it — on the loop's first iteration instead of polling. It only ever worked when launched from an interactive shell where Xwayland happened to already be up (so pgrep matched on try 1). Under the systemd boot appliance (punktfunk-kde-session.service) Xwayland isn't up that early, so the session crash-looped (restart counter climbing, KWin never staying), the host had no compositor, and clients couldn't connect. Append `|| true` to the substitution so the loop polls as intended and a session with no Xwayland at all still proceeds (DISPLAY just stays unset → warn). Verified live: the unit now stays active (0 restarts), KWin + the wayland-kde socket persist, probe-compositor reports ready, and a real client session captured 4.8 MB of H.265 off the running serve --native host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f3ff5f648a |
fix(headless-kde): complete the bare session — export DISPLAY, polkit agent, supervise plasmashell
ci / rust (push) Has been cancelled
A full Plasma login starts several pieces our bare headless session was missing, which
surfaced as three separate failures while streaming the KDE desktop:
- Steam (and other X11 apps) failed "can't open display": Xwayland runs, but KWin only
sets DISPLAY for its own children — apps launched via the plasma menu / D-Bus activation
never saw it. Detect the Xwayland display after KWin is ready and export it into the
systemd/D-Bus activation environment.
- Discover / PackageKit couldn't install apps: polkitd (the policy engine) was running but
no authentication *agent* (the prompt) was — so privileged installs got no authorization.
Start polkit-kde-authentication-agent-1 (forcing the Qt Wayland platform, or it exits).
- The streamed desktop showed app windows but no wallpaper/panels: plasmashell had crashed
and the old unsupervised `plasmashell &` never brought it back. Supervise it — restart for
as long as KWin lives, so the desktop shell self-heals.
Validated live on this box: DISPLAY=:0 now in the --user environment (xdpyinfo on :0 works),
the polkit agent registers ("Listener online"), and plasmashell stays up under the supervisor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
49d31b9cad |
fix(headless-kde): --no-block the portal restart so bring-up isn't blocked ~30s
ci / rust (push) Has been cancelled
A synchronous systemctl try-restart of the portal chain (xdg-desktop-portal is Type=dbus, waits for its bus name) blocked the script ~30-40s before plasmashell started. --no-block queues the restart and returns immediately — the portal only needs to be ready before the first client streams (seconds later), not before plasmashell. Validated: plasmashell up in 1s (was ~30s); a virtual capture session against the fresh session streamed 720/720 frames @720p120, zero-copy CUDA, no black screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9fdc3c3246 |
feat(headless-kde): reliable bring-up — readiness probe, fix portal ordering/env (roadmap #1 phase 1)
ci / rust (push) Has been cancelled
Headless KDE startup was a chain of timing-sensitive handoffs gated by a blind `sleep 2`, the dominant source of black screens. Phase-1 fixes: - New `punktfunk-host probe-compositor` subcommand: exits 0 iff the detected compositor is up AND ready to create a virtual output now. KWin gets a real check (connect + registry roundtrip + the privileged zkde_screencast global must be advertised — what the backend needs); gamescope/Mutter/wlroots create on demand so the probe just confirms Linux. (vdisplay::probe dispatcher + kwin::probe; reuses kwin.rs's existing roundtrip path.) - run-headless-kde.sh: replace `sleep 2` with an active readiness wait (poll probe-compositor until ready, 30s deadline, and bail with kwin's log if kwin_wayland exits during init). Move the portal restart to AFTER readiness, and precede it with `systemctl --user import-environment` + `dbus-update-activation-environment` (the missing env import — the Sway script does this; without it a restarted portal inherits a stale/empty WAYLAND_DISPLAY, which is the "streams but eats no input/audio" failure). kwin's stderr → a log file. Validated: probe-compositor exits 0 "Kwin ready" against the live session, exit 1 with a clear diagnostic when the compositor is absent. 114 tests green, clippy/fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7381ba8218 |
feat(vdisplay): wlroots/Sway backend — swaymsg headless output + xdpw chooser
The fourth VirtualDisplay backend: `swaymsg create_output` adds a HEADLESS-N output (name found by diffing get_outputs), `output <NAME> mode --custom WxH@HzHz` sets the client's exact mode (and the refresh clock a fresh headless output needs to produce frames at all), and the PipeWire node comes from the ScreenCast portal. Headless output selection is non-interactive via xdg-desktop-portal-wlr's chooser hook: a managed config (chooser_type=simple, chooser_cmd cats /tmp/punktfunk-xdpw-output; portal try-restarted when the config changes) plus a per-session `Monitor: <NAME>` written to that file. Teardown is RAII: drop ends the portal thread (zbus connection drop ends the cast) then `swaymsg output <NAME> unplug`. swaymsg commands go after `--` so tokens like `--custom` reach sway instead of swaymsg's getopt. Validated live on headless sway 1.11 (gles2-on-NVIDIA, xdpw 0.8.1), zero-copy dmabuf→CUDA on both runs: 720p60 257 frames p50 0.77 ms, 1080p60 480/480 frames p50 1.18 ms, output unplugged with the session both times. The checked-in xdpw.config sample now matches the managed config (the old chooser_type=none/HEADLESS-1 form would pin capture to the wrong output). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
187c173e01 |
fix(headless-kde): restart xdg-desktop-portal after kwin comes up
ci / rust (push) Has been cancelled
The portal processes bind to the compositor that existed when they started; after a kwin
restart the stale instances point at a dead socket and RemoteDesktop/EIS input injection
times out ("EIS setup timed out"). Hit live: a fresh session streamed fine but ate no
mouse/keyboard until the portals were restarted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bfd64ce871 |
rename: lumen → punktfunk, everywhere
ci / rust (push) Has been cancelled
Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab6dda2e5f |
feat: M0 capture→encode pipeline + M2 GameStream host (pairing, RTSP, video)
M0 (lumen-host) — verified on NVIDIA RTX 5070 Ti / Ubuntu 25.10: headless wlroots → xdg ScreenCast portal → PipeWire → NVENC HEVC → playable file, with each access unit round-tripped through a lumen_core host↔client Session (FEC + packetize + reassemble), 0 mismatches. - capture.rs: SyntheticCapturer + portal capture (ashpd 0.13 + pipewire 0.9), format-aware - encode/linux.rs: NVENC via ffmpeg-next 7 (BGRx/RGB → rgb0, no host-side swscale) - m0.rs: capture→encode→file + lumen-core loopback verification M2 P1 (lumen-host gamestream/) — a stock Moonlight client pairs + launches, verified live: - mDNS _nvstream._tcp + nvhttp /serverinfo (HTTP 47989, mutual-TLS HTTPS 47984) - 4-phase pairing: PIN→AES-128-ECB / SHA-256 / RSA-PKCS1v15 / X.509, custom rustls ClientCertVerifier for the mutual-TLS pairchallenge - /applist, /launch (rikey/rikeyid/mode), hand-rolled RTSP (OPTIONS/DESCRIBE/SETUP×3/ ANNOUNCE/PLAY, one-request-per-TCP-connection per moonlight-common-c's read-to-EOF) - video.rs: GameStream RTP + NV_VIDEO_PACKET wire packetizer, data-shards-only (0% FEC, clean-LAN), unit-tested (single/multi-block) Docs: docs/m2-plan.md (phased plan) + docs/research/ (ground-truth protocol spec). Bootstrap/setup updated for the verified path (libnvidia-gl, render/video groups, GPU EGL, pipewire 0.9). Workspace clippy-clean, tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b0172d793 |
docs: VM handoff — CLAUDE.md, Ubuntu bootstrap, headless-Sway setup for M0
Prepares the move to the NVIDIA-GPU Ubuntu VM where M0/M2 run (macOS can't drive the Wayland/GPU stack). The repo carries the context, since Claude Code sessions are machine-local and don't transfer. - CLAUDE.md: project state + design invariants + don't-regress security notes. Auto-loads every session, so a fresh session on the VM continues from here. - scripts/bootstrap-ubuntu.sh: verifies the (already-installed) NVIDIA/NVENC stack, installs rustup + PipeWire/portal/wlroots/Sway + DRM/EGL/GBM/VA dev deps; GATES the FFmpeg -dev headers so apt can't clobber a custom NVENC build; checks nvidia-drm.modeset. - scripts/headless/: headless-Sway + xdg-desktop-portal-wlr config templates, the NVIDIA-wlroots env workarounds, run-headless-sway.sh, and a wf-recorder->hevc_nvenc capture smoke test (proves capture->NVENC with no Rust). - docs/linux-setup.md: M0 walkthrough + verified gotchas (modeset, headless backend, vGPU NVENC licensing, dmabuf->NVENC CPU-copy fallback, FFmpeg-dev gate, crate versions). Ubuntu 24.04 package names/versions verified against the live archive; scripts pass shellcheck and `bash -n`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |