You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Choose the shell's runtime and distribution given OpenTUI's FFI requirement #59
OpenTUI's renderer runs only on Node 26.4.0+ with --experimental-ffi, or on Bun (bun:ffi, flag-free) — Node 24 LTS cannot run it at all. This was measured while landing #55 and it contradicts the runtime decision in #21/#47 (Node 24 LTS npm package), which rested on the #6 prototype's now-disproven "Node needs no FFI" conclusion.
Given that, what runtime and distribution should Secant commit to for the interactive shell — a Node 26.4+FFI npm package, a Bun-compiled zero-install binary (OpenCode's model), or a Node SEA — and what does that decision force to change elsewhere (the SQLite driver, the distribution model, the CI Node line, and the tickets/ADRs built on Node 24)?
OpenTUI 0.4.5's renderer loads its native lib through node:ffi. The platform package @opentui/core-<platform>-<arch> ships a raw libopentui.dylib/.so/.dll (not an N-API .node addon), so it can only be dlopen'd via FFI. The latest OpenTUI (0.5.11) is the same — no N-API path exists.
node:ffi / --experimental-ffi first appears in official Node 26.1.0; OpenTUI pins 26.4.0. Node 24 LTS and Node 26.0.0 reject --experimental-ffi as a bad option. Verified by running createTestRenderer across Node builds: it renders only on 26.4.0+ with the flag, or under Bun (bun:ffi, no flag).
Timing: Node 26 becomes Active LTS on 2026-10-28; Secant is unpublished ("nothing is published yet"). ADR 0018's runtime-neutral vendoring (no Bun.*/bun:* in target code) was designed precisely to keep the Bun route open — the vendored TUI already runs on either runtime unchanged.
The options on the table
Node 26.4.0+ npm package + transparent --experimental-ffi re-exec (what Open the shell: Workspace approval and Home in the TUI on the Renderer Port #55 landed provisionally). The CLI re-execs itself with --experimental-ffi --conditions=browser when launching the TUI, so users never type a flag — but they must have Node 26.4.0+ installed. Above the current LTS until 2026-10-28; the flag is experimental (no cross-patch stability guarantee).
Bun-compiled single-file executable (OpenCode's exact, proven model). bun build --compile embeds the Bun runtime and the OpenTUI native lib into one per-platform binary; OpenTUI runs flag-free via bun:ffi. The user installs nothing. Distributed via a curl installer, GitHub-release archives, a thin npm launcher whose per-platform optionalDependencies carry the binary (so npm i -g still works), Homebrew, etc. ~50–100 MB per-platform binaries, cross-compiled from one host.
Node SEA embedding Node 26.4 and baking --experimental-ffi via execArgvExtension:"cli". User installs nothing, but it stacks three experimental features (Node 26.4 runtime + node:ffi + SEA), has no cross-compilation, and is materially more fragile.
@yao-pkg/pkg (ships Node 26.5 prebuilt) — the FFI + flag path is unproven for pkg; treat as fallback only.
What the choice forces to change
SQLite driver (reopens Q8).Bun does not implement node:sqlite (No such built-in module: node:sqlite). The Catalog (Approve and show the Workspace headlessly over the workspace Projection #50) uses node:sqlite. A Bun path forces SQLite onto bun:sqlite or better-sqlite3 behind a conditional import (OpenCode's #db map pattern). A Node-26.4 path keeps node:sqlite as-is.
The #55 record comment; docs/research/opencode-tui-extraction-constraints.md (the "Bun, Node, OpenTUI, And Solid Constraints" section); Node release notes for 26.1.0 and 26.4.0; OpenTUI's runtime-support docs; Bun's single-file-executable docs; and the direct createTestRenderer-across-Node-versions probe run during #55.
💭 Fleeting note (parking-lot, not part of this decision): every milestone should carry one dedicated code-review pass whose remit is architecture — module depth and seams, folder/import topology, dependency direction, and adherence to the standing principles (deep modules, no dead UI, runtime-neutrality, etc.) — kept separate from correctness/spec review. Worth turning into its own milestone-loop step (or a gate) rather than leaving it implicit in per-ticket change review. Recording it here so it isn't lost; it wants its own home later.
Not a resolution. This records what fact-finding established before the decision is taken, so the resolution can cite it rather than restate it. Every external claim below was checked against a primary source today; anything unverified is marked.
1. Corrections to the Starting context
Starting-context claim
What the sources say today
Bun does not implement node:sqlite
Bun 1.4.0 (2026-08-20) implements it fully (compat table, PR #32498). One open Windows bug: close() can leave the file locked (#40001). A Bun path no longer forces the SQLite driver change; Q8 decouples from this decision.
--experimental-ffi is an unstable flag users are stuck with
node:ffi landed in 26.1.0, not 26.4. Node merged "ffi: enable module by default" on 2026-08-28 (PR #65475); not in 26.8.2 (released 2026-09-09), so expect 26.9.0. The flag becomes a no-op; the module still emits an ExperimentalWarning, which --disable-warning=ExperimentalWarning silences. Stability index today: 1 (Experimental).
OpenTUI's Node path is unofficial
OpenTUI's runtime-support page lists Node.js 26.4.0+ with ESM and --experimental-ffi as supported; engines is { bun: ">=1.3.0", node: ">=26.4.0" }. Its Node tests run on Linux only. The N-API request (opentui#2) was closed as completed via the node:ffi route.
Node SEA is CommonJS-only and cannot cross-compile
Node 26 SEA supports ESM (mainFormat: "module", since 25.7.0) and cross-compilation via the executable field (with useCodeCache/useSnapshot off). execArgv bakes --experimental-ffi (verified locally: the bytes appear in the blob). Still Stability 1.1; the native library must be written to a temp dir before dlopen.
Bun embeds OpenTUI's library cleanly
It works but is undocumented: Bun materializes an embedded shared library to a tmpfile before dlopen (PR #30720). Broken from 1.3.14-canary through stable 1.3.14, fixed in 1.4.0. Bun's own FFI page: "Do not rely on it in production" (docs).
Node 26 LTS 2026-10-28
Confirmed (schedule.json). Node 24 EOL 2028-04-30. Latest 26.x is 26.8.2.
Two repo facts the Starting context did not carry:
ADR 0018 measured that Bun forfeits legacy conhost. Under Bun 1.3.14 the stdin-release workaround does not save the console. The only source is opentui#1405 (filed from this project); its 2026-09-06 follow-up shows that on Node it is closing the stdin handle that saves conhost, not keeping it alive. Bun's destroy() does not release the native reader; the fix is an open, unmerged Bun PR (#35621).
A single-file artefact breaks three standing decisions as written: ADR 0029 finds built-in .wfb files beside dist/, scripts/package-smoke.mjs installs an npm tarball, and ADR 0027 publishes to npm from CI.
2. OpenTUI 0.4.5 on the current Node line (measured today, this repo)
Node
Full suite (node scripts/run-tests.mjs)
Direct FFI probe
--disable-warning=ExperimentalWarning
26.4.0 (declared floor)
79/79
ok
suppresses
26.7.0
79/79
ok
suppresses
26.8.2 (latest)
79/79, three runs
ok
suppresses
The three node:ffi tightenings in 26.7–26.8 (SharedArrayBuffer pointers, detached ArrayBuffers, calls after close()) are not hit by OpenTUI 0.4.5. Without the flag on 26.8.2 the renderer still fails ("OpenTUI native FFI is not available for this runtime yet"), i.e. unflagging has not shipped.
3. What the peer CLIs did
CLI
Distribution today
Stated reason
Claude Code
Native installer recommended, compiled with Bun (Bun 1.4.1 notes); README calls npm install deprecated (registry does not flag it). Anthropic acquired Bun 2025-12.
Docs argue auto-update and npm permission problems; "no Node dependency" is not a documented differentiator (setup).
Codex
Rust binary, curl installer, npm wrapper.
Original Rust README: "requires Node.js 22+ … this runtime requirement inhibits adoption: they would be better served by a standalone executable" (31d0d7a3).
Gemini CLI
npm only, Node ≥ 20.
None stated.
Also documented: under nvm, global npm packages live per Node version (nvm README), so a global CLI disappears on version switch; nvm ships reinstall tooling for exactly this.
4. Bun downsides Node does not have (verified)
Windows crashes in compiled Bun TUIs, open and unanswered.bun#27471 (N-API vtable corruption, filed against Claude Code's bundled Bun: 49 crashes in 33 days on Bun vs zero on Node, TUI escape into the host shell) — open, no Bun reply. bun#40302 (1.4.0 Windows standalone segfault in a bun:ffi loop through @opentui/core, filed by an OpenCode maintainer) — probably fixed by the WebKit bump in 1.4.1, still open, unconfirmed. bun#42099 (1.4.1 standalone segfaults) opened 2026-09-09.
Legacy conhost as above: real, with an unmerged fix path.
Signing is geography-gated. Azure Artifact Signing (what OpenCode uses for Windows) admits organizations in US/CA/EU/UK/AU/NZ/JP/KR/SG/CH/NO/IL only; individuals US/CA only (quickstart); from $9.99/month. macOS needs the $99/yr Developer Program plus notarization. Claude Code signs and notarizes both; OpenCode signs Windows only and ships macOS CLI binaries under Bun's own identity (opencode#46313). curl does not set the macOS quarantine attribute, but XProtect and signature-validity checks run regardless.
Patch churn on the developer side. 1.4.0 broke dns.lookup on macOS (#40573); 1.4.1 broke bun build output for a day (#41351); embedded-dylib dlopen broken across 1.3.14; better-sqlite3 v13 aborted Bun until 1.4.1. Users never see this (the binary pins the runtime); every Bun upgrade is a soak.
Test toolchain.node:test under bun test is partial (28/81 upstream tests per the 1.4 blog); module.register/registerHooks are silent no-ops, so the Babel/Solid test loader must move to @opentui/solid/preload. Bun.Terminal (1.3.5, Windows via ConPTY in 1.4) could replace test-only node-pty, which does not run on Bun at all (bun#7362, closed not planned).
Artefact size. OpenCode v1.18.30: 44–60 MB compressed per platform, 138–176 MB unpacked through npm. Bun's own binary floor is ~59 MB.
FFI and embedding are officially experimental/undocumented (section 1).
5. Node downsides (for symmetry)
Users must install Node ≥ 26.4 (LTS in seven weeks). nvm users lose the global on version switch. No zero-dependency installer, ever. Everything else about the Node path is temporary (flag → 26.9, warning → one switch, LTS → 2026-10-28).
6. How OpenCode handles each downside (source read at anomalyco/opencode 1ead9e3d, v1.18.25)
Downside
OpenCode's handling
Conhost wedge
None. Nothing releases stdin; exit is renderer destroy → worker shutdown RPC (5 s timeout) → process.exit, the exact post-destroy() event-loop work #1405 measured as fatal. Eight user reports of "console dead after exit" on Windows (#22003, #23720, #25691, #26480, #27749, #28155, #28673, #30495), all stale-closed; six community fix PRs closed unmerged. Docs recommend WSL on Windows.
Windows crashes
No telemetry, no crash handling, no restart. Response was toolchain: CI moved off Bun canary to stable baseline builds (Feb 2026), native file search disabled on Windows by default (Jun 2026), Bun frozen at 1.3.14 (last pre-Rust release) since 2026-05-18.
Signing / AV
Windows via azure/artifact-signing-action since March; macOS CLI unsigned. Maintainer's stated AV root cause: the binary extracts OpenTUI's library to a temp folder on first start (#7919); signing did not stop the reports.
Patch churn
Ten Bun pin changes in four months incl. two rollbacks, then frozen. minimumReleaseAge 3 days with all @opentui/* exempted. Windows CI needs --linker hoisted for bun#28147. OpenTUI upgrades reverted twice.
Testing
bun test everywhere; TUI tested headlessly with createTestRenderer/testRender (same as this repo). No PTY test, no test of the compiled binary beyond --version. The TUI test package is not in the CI turbo graph.
Embedding
bun install --os="*" --cpu="*" for the native packages, then Bun.build({ compile: { target } }) per target; bunfs root switched per OS; one vendor package hand-patched so the compiler can see its library.
Claude Code (public tracker/docs only): no report of the wedge symptom; docs claim Windows 10 1809+ and "works in any terminal without configuration"; two 2026 changelog entries about restoring terminal modes on exit; one open conhost rendering bug (#88306); no changelog line about a Windows crash fix; Bun's team redirects Claude Code crash reports to Anthropic because it ships its own Bun build.
7. Sizing legacy conhost
Definition in this repo's record (docs/research/opentui-windows-console-destroy.md, ADR 0027): a visible window owned by conhost.exe, not the "Use legacy console" checkbox (deprecated 2023, no longer installed by default).
Windows 11 22H2+ (2022-10): Windows Terminal is the default host (announcement). Windows Terminal, VS Code, Cursor, JetBrains, SSH, and WSL launched inside any of those run conhost headless behind ConPTY — the wedge cannot fire there.
Still a conhost window on Windows 11: elevated launches (by design, terminal#13392), explicit conhost.exe, handoff failures, users who set the default host back to Console Host.
Windows 10: Windows Terminal never preinstalled or default; the default-host setting needs 22H2 + KB5026435. Same for Server 2022/2025 and LTSC.
Windows 10 share, Statcounter Aug 2026, all desktop Windows: 30.1 % worldwide, 23.5 % US (source); consumer ESU extended to 2027-10-12.
No developer-specific Windows-version or terminal-emulator data exists (Stack Overflow 2025, JetBrains 2025, Octoverse 2025 all lack it). Microsoft has never published a Windows Terminal install base.
Microsoft's plan: retire conhost "from its default position", never remove it (roadmap).
Inference: the exposed population is Windows 10 developers who launch from a bare cmd/PowerShell window rather than an IDE terminal, plus anyone running elevated. 30 % is an upper bound on the first group; the true share is well below it and shrinking. The cheapest handling under Bun is detection (refuse the TUI with a message when not under Windows Terminal/an IDE; headless unaffected) rather than support.
8. Where the discussion stands
Recommendation moved from Node 26.4+ npm to Bun-compiled binary, on the permanence argument (Node's problems are temporary except install friction; Bun's are one-time build costs plus a managed Windows crash risk), three of four peers shipping standalone binaries, and a conhost row that is small and detectable.
Open questions on the table: Q1 runtime (Bun all-in vs Node vs staged); Q7 a HITL Windows soak of a Bun 1.4.2 baseline build of the current shell before the ADR is written; Q8 conhost policy (support / detect-and-refuse / drop).
Secant's interactive shell ships as a Bun-compiled single-file executable, not a Node npm package. This overturns the runtime and distribution half of #21 ("Node 24 LTS, one npm package, no self-contained executable in v1"), which rested on #6's disproven "Node needs no FFI" measurement. The decision is taken now; ADR 0030 is written when the Windows soak in #60 passes (a fail on Bun 1.4.2 and again on the next Bun release reverts to the Node 26.4+ npm path with the transparent --experimental-ffi re-exec that #55 landed).
Why Bun over Node 26.4: Node's problems are temporary (the flag is a no-op from 26.9, LTS lands 2026-10-28) except the permanent ones — users must install a specific Node, nvm users lose the global on version switch, and a zero-dependency installer is never possible. Three of the four peer CLIs (Claude Code, Codex, OpenCode) ship standalone binaries and Codex wrote down exactly this reason. Bun's costs are one-time build work plus a Windows crash family that is real, open, and managed by pinning (see the findings); the soak measures it before the ADR.
Decisions
Runtime and toolchain. Bun pinned exactly (packageManager, 1.4.2 or the release Soak the Bun-compiled shell on a real Windows machine before ADR 0030 #60 passes on); CI reads the pin. A Bun bump goes through a PR that passes the three-OS gate and re-arms the human Windows check, exactly as ADR 0027 treats an OpenTUI bump. One toolchain: bun build --compile, bun test with @opentui/solid/preload, Bun.Terminal for the real-terminal suite; tsup, tsx, the Babel test loader, and test-only node-pty go; tsc, ESLint, Prettier stay. Baseline x64 builds are moot in Bun 1.4 (AVX2 chosen at runtime).
Distribution, v1. GitHub release archives for exactly the three gated targets (Windows x64, macOS arm64, Linux x64; extras are one line each when asked and each is a row someone will assume is tested). curl | sh and PowerShell installers into ~/.secant/bin on every platform — a fixed location, independent of SECANT_HOME (ADR 0023), no override variable, matching OpenCode's fixed ~/.opencode/bin. An npm launcher @secantdev/secant that spawns the binary from a per-platform optionalDependencies package with no postinstall (Claude Code's model, so --ignore-scripts and pnpm cannot break it); Node is needed only to run npm and the shim, any version. No Homebrew, winget, scoop, or Docker in v1. No self-updater and no version check in v1 (unchanged from Choose Crucible's process, runtime, packaging, and distribution architecture #21).
Signing, v1. macOS binaries carry Bun's automatic ad-hoc signature (Apple silicon refuses arm64 code without at least that) and CI runs codesign --verify --deep --strict on every macOS artefact because Bun's re-signing has broken twice (1.3.12, 1.3.13). No Developer ID, no notarization, no Windows Authenticode in v1. curl does not set the quarantine flag and tar does not propagate it, so the curl and npm routes are never Gatekeeper-evaluated; release-page downloads are documented as "run from Terminal, do not double-click". curl-installed Windows executables carry no mark-of-the-web, so SmartScreen does not fire on them (Smart App Control machines excepted).
SQLite. The Catalog and Run Store use bun:sqlite behind their Interfaces. Bun 1.4 implements node:sqlite but its Windows close() file-lock bug hits the Catalog's open-per-command pattern; bun:sqlite is Bun's first-class API with a transaction helper. Q8 (Choose Crucible's process, runtime, packaging, and distribution architecture #21 §6) closes with this decision: better-sqlite3 is moot under Bun (it needed a Bun fix to load at all and is special-cased by filename).
Runtime neutrality. ADR 0018's rule extends from vendored presentation source to all target source, with a named allowlist where bun:/Bun. are permitted: the SQLite adapter and the Windows console guard. The mechanical check in tests/architecture/check-module-boundaries.ts becomes an allowlist instead of a blanket ban. No Node twin implementations (OpenCode's #sqlite/#pty/#fff node sides exist only for its Electron app and are untested). Evidence that this is achievable: OpenCode touches Bun APIs in 12 of 833 source files, 25 of its 33 call sites are Bun.stringWidth, and it cut from 321 sites in Feb 2026 to 48 by replacing Bun calls with Node builtins.
Legacy conhost. Dropped as a support-matrix row. Startup prints a one-line notice when the console window is conhost-owned (bun:ffiGetConsoleWindow + IsWindowVisible; ConPTY's headless conhost has an invisible window) pointing at Windows Terminal; the TUI still runs and headless is unaffected. Rationale: the wedge is exit-only (a dead console window, no Secant data lost), the exposed population is Windows 10 users launching from a bare cmd/PowerShell window plus elevated shells (upper bound 30 % of desktop Windows, true developer share well below), and OpenCode ships with no handling and eight stale-closed bug reports for it — the notice is what prevents that noise. The human release check retargets from conhost to Windows Terminal, run when the Bun pin, the OpenTUI pin, or the renderer changed. When Bun merges the stdin-release fix (bun#35621) the notice can go.
Gate before the ADR.Soak the Bun-compiled shell on a real Windows machine before ADR 0030 #60 (HITL Task): compile the current shell for bun-windows-x64 on Bun 1.4.2 and run it on a real Windows machine through every launch and exit path in Windows Terminal plus one long session and a 20-launch loop. Conhost is observed and recorded, not deciding.
ADR 0027 amendment — release artefacts are per-platform binaries plus the npm launcher; a Bun bump re-arms the human Windows Terminal check; the conhost row is removed.
ADR 0029 amendment — built-in .wfb files are embedded in the binary as assets and installed at startup from the embedded bytes; everything else unchanged.
Developer ID + notarization (US$99/yr program; individuals need no D-U-N-S) and Windows Authenticode (Azure Artifact Signing is geography-gated; otherwise a CA certificate) — required before a Homebrew channel (casks must pass Gatekeeper since 2026-09-01) or browser-download support.
secant upgrade — must write a new file and rename, never overwrite in place (macOS caches the signature in the kernel; in-place overwrite crashes on next launch until reboot).
Extra targets (macOS x64, Linux arm64/musl, Windows arm64), Homebrew/winget/scoop, passive update notice, dropping the conhost notice once Bun fixes stdin release.
OpenCode's installer hardcodes $HOME/.opencode/bin (install:68) with no override variable; its data/config/cache/state follow XDG via xdg-basedir (packages/core/src/global.ts); it has no PowerShell installer (Windows: npm/bun/choco/scoop, WSL recommended).
OpenCode runtime coupling: 33 Bun.* sites in 12 of 833 files; three bun/node import-map seams in packages/core/package.json; the Node side is exercised only by the Electron desktop build (script/build-node.ts) and untested; 321 → 48 sites since Feb 2026 via two deliberate migration campaigns.
Apple silicon requires at least an ad-hoc signature (Platform Security guide); curl/tar do not set or propagate quarantine (Apple DTS); Bun's ad-hoc re-signing regressed in 1.3.12 (#29120) and 1.3.13 (#32159); Homebrew ended support for casks failing Gatekeeper on 2026-09-01 (brew#20755); in-place binary replacement hazard (Apple).
Activity
Sandstorm831 commentedon Sep 10, 2026
Starting context
Curated from the #55 slice, which is where this surfaced. Everything below is evidence for the grilling session; nothing here is decided.
The forcing fact (measured during #55)
0.4.5's renderer loads its native lib throughnode:ffi. The platform package@opentui/core-<platform>-<arch>ships a rawlibopentui.dylib/.so/.dll(not an N-API.nodeaddon), so it can only bedlopen'd via FFI. The latest OpenTUI (0.5.11) is the same — no N-API path exists.node:ffi/--experimental-ffifirst appears in official Node 26.1.0; OpenTUI pins 26.4.0. Node 24 LTS and Node 26.0.0 reject--experimental-ffias a bad option. Verified by runningcreateTestRendereracross Node builds: it renders only on 26.4.0+ with the flag, or under Bun (bun:ffi, no flag).terminal-win32.ts), not the renderer's own native load, which does require FFI. So the Node-24 floor in Choose Crucible's process, runtime, packaging, and distribution architecture #21/Cut legacy DevFlow and rebrand to Secant on a Node 24 three-OS gate #47 does not actually run the TUI. The Prove an OpenCode-derived Crucible shell across Windows, macOS, and Linux #6 record should be corrected as part of this decision.Bun.*/bun:*in target code) was designed precisely to keep the Bun route open — the vendored TUI already runs on either runtime unchanged.The options on the table
--experimental-ffire-exec (what Open the shell: Workspace approval and Home in the TUI on the Renderer Port #55 landed provisionally). The CLI re-execs itself with--experimental-ffi --conditions=browserwhen launching the TUI, so users never type a flag — but they must have Node 26.4.0+ installed. Above the current LTS until 2026-10-28; the flag is experimental (no cross-patch stability guarantee).bun build --compileembeds the Bun runtime and the OpenTUI native lib into one per-platform binary; OpenTUI runs flag-free viabun:ffi. The user installs nothing. Distributed via a curl installer, GitHub-release archives, a thin npm launcher whose per-platformoptionalDependenciescarry the binary (sonpm i -gstill works), Homebrew, etc. ~50–100 MB per-platform binaries, cross-compiled from one host.--experimental-ffiviaexecArgvExtension:"cli". User installs nothing, but it stacks three experimental features (Node 26.4 runtime +node:ffi+ SEA), has no cross-compilation, and is materially more fragile.@yao-pkg/pkg(ships Node 26.5 prebuilt) — the FFI + flag path is unproven for pkg; treat as fallback only.What the choice forces to change
node:sqlite(No such built-in module: node:sqlite). The Catalog (Approve and show the Workspace headlessly over theworkspaceProjection #50) usesnode:sqlite. A Bun path forces SQLite ontobun:sqliteorbetter-sqlite3behind a conditional import (OpenCode's#dbmap pattern). A Node-26.4 path keepsnode:sqliteas-is.bin, no self-contained executable in v1." Options 2 and 3 overturn that.engines.nodeto26.4.0as a stopgap. A Bun path replaces the pinned-Node CI with a Bun toolchain; a Node path keeps it but above LTS (for now).bun build --compileand drops the re-exec entirely.Other issues / decisions to rethink
engines/tsup/CI).node-ptymust build on the chosen runtime. Itsready-for-agentis withheld pending this decision.node:sqlitevsbetter-sqlite3) — a Bun path forces the driver.engines/CI are at26.4.0until this settles.Evidence sources
The #55 record comment;
docs/research/opencode-tui-extraction-constraints.md(the "Bun, Node, OpenTUI, And Solid Constraints" section); Node release notes for 26.1.0 and 26.4.0; OpenTUI's runtime-support docs; Bun's single-file-executable docs; and the directcreateTestRenderer-across-Node-versions probe run during #55.Sandstorm831 commentedon Sep 10, 2026
💭 Fleeting note (parking-lot, not part of this decision): every milestone should carry one dedicated code-review pass whose remit is architecture — module depth and seams, folder/import topology, dependency direction, and adherence to the standing principles (deep modules, no dead UI, runtime-neutrality, etc.) — kept separate from correctness/spec review. Worth turning into its own milestone-loop step (or a gate) rather than leaving it implicit in per-ticket change review. Recording it here so it isn't lost; it wants its own home later.
Sandstorm831 commentedon Sep 10, 2026
Grilling findings (so far, 2026-09-10)
Not a resolution. This records what fact-finding established before the decision is taken, so the resolution can cite it rather than restate it. Every external claim below was checked against a primary source today; anything unverified is marked.
1. Corrections to the Starting context
node:sqliteclose()can leave the file locked (#40001). A Bun path no longer forces the SQLite driver change; Q8 decouples from this decision.--experimental-ffiis an unstable flag users are stuck withnode:ffilanded in 26.1.0, not 26.4. Node merged "ffi: enable module by default" on 2026-08-28 (PR #65475); not in 26.8.2 (released 2026-09-09), so expect 26.9.0. The flag becomes a no-op; the module still emits anExperimentalWarning, which--disable-warning=ExperimentalWarningsilences. Stability index today: 1 (Experimental).--experimental-ffias supported;enginesis{ bun: ">=1.3.0", node: ">=26.4.0" }. Its Node tests run on Linux only. The N-API request (opentui#2) was closed as completed via thenode:ffiroute.mainFormat: "module", since 25.7.0) and cross-compilation via theexecutablefield (withuseCodeCache/useSnapshotoff).execArgvbakes--experimental-ffi(verified locally: the bytes appear in the blob). Still Stability 1.1; the native library must be written to a temp dir beforedlopen.dlopen(PR #30720). Broken from 1.3.14-canary through stable 1.3.14, fixed in 1.4.0. Bun's own FFI page: "Do not rely on it in production" (docs).Two repo facts the Starting context did not carry:
destroy()does not release the native reader; the fix is an open, unmerged Bun PR (#35621)..wfbfiles besidedist/,scripts/package-smoke.mjsinstalls an npm tarball, and ADR 0027 publishes to npm from CI.2. OpenTUI 0.4.5 on the current Node line (measured today, this repo)
node scripts/run-tests.mjs)--disable-warning=ExperimentalWarningThe three
node:ffitightenings in 26.7–26.8 (SharedArrayBuffer pointers, detached ArrayBuffers, calls afterclose()) are not hit by OpenTUI 0.4.5. Without the flag on 26.8.2 the renderer still fails ("OpenTUI native FFI is not available for this runtime yet"), i.e. unflagging has not shipped.3. What the peer CLIs did
Also documented: under nvm, global npm packages live per Node version (nvm README), so a global CLI disappears on version switch; nvm ships reinstall tooling for exactly this.
4. Bun downsides Node does not have (verified)
bun:ffiloop through @opentui/core, filed by an OpenCode maintainer) — probably fixed by the WebKit bump in 1.4.1, still open, unconfirmed. bun#42099 (1.4.1 standalone segfaults) opened 2026-09-09.dns.lookupon macOS (#40573); 1.4.1 brokebun buildoutput for a day (#41351); embedded-dylibdlopenbroken across 1.3.14; better-sqlite3 v13 aborted Bun until 1.4.1. Users never see this (the binary pins the runtime); every Bun upgrade is a soak.node:testunderbun testis partial (28/81 upstream tests per the 1.4 blog);module.register/registerHooksare silent no-ops, so the Babel/Solid test loader must move to@opentui/solid/preload.Bun.Terminal(1.3.5, Windows via ConPTY in 1.4) could replace test-onlynode-pty, which does not run on Bun at all (bun#7362, closed not planned).5. Node downsides (for symmetry)
Users must install Node ≥ 26.4 (LTS in seven weeks). nvm users lose the global on version switch. No zero-dependency installer, ever. Everything else about the Node path is temporary (flag → 26.9, warning → one switch, LTS → 2026-10-28).
6. How OpenCode handles each downside (source read at
anomalyco/opencode1ead9e3d, v1.18.25)process.exit, the exact post-destroy()event-loop work #1405 measured as fatal. Eight user reports of "console dead after exit" on Windows (#22003, #23720, #25691, #26480, #27749, #28155, #28673, #30495), all stale-closed; six community fix PRs closed unmerged. Docs recommend WSL on Windows.azure/artifact-signing-actionsince March; macOS CLI unsigned. Maintainer's stated AV root cause: the binary extracts OpenTUI's library to a temp folder on first start (#7919); signing did not stop the reports.minimumReleaseAge3 days with all@opentui/*exempted. Windows CI needs--linker hoistedfor bun#28147. OpenTUI upgrades reverted twice.bun testeverywhere; TUI tested headlessly withcreateTestRenderer/testRender(same as this repo). No PTY test, no test of the compiled binary beyond--version. The TUI test package is not in the CI turbo graph.bun install --os="*" --cpu="*"for the native packages, thenBun.build({ compile: { target } })per target; bunfs root switched per OS; one vendor package hand-patched so the compiler can see its library.Claude Code (public tracker/docs only): no report of the wedge symptom; docs claim Windows 10 1809+ and "works in any terminal without configuration"; two 2026 changelog entries about restoring terminal modes on exit; one open conhost rendering bug (#88306); no changelog line about a Windows crash fix; Bun's team redirects Claude Code crash reports to Anthropic because it ships its own Bun build.
7. Sizing legacy conhost
Definition in this repo's record (
docs/research/opentui-windows-console-destroy.md, ADR 0027): a visible window owned byconhost.exe, not the "Use legacy console" checkbox (deprecated 2023, no longer installed by default).conhost.exe, handoff failures, users who set the default host back to Console Host.Inference: the exposed population is Windows 10 developers who launch from a bare cmd/PowerShell window rather than an IDE terminal, plus anyone running elevated. 30 % is an upper bound on the first group; the true share is well below it and shrinking. The cheapest handling under Bun is detection (refuse the TUI with a message when not under Windows Terminal/an IDE; headless unaffected) rather than support.
8. Where the discussion stands
node:sqliteworks on Bun;bun:sqliteis an option), a new ADR superseding Choose Crucible's process, runtime, packaging, and distribution architecture #21's Node-24 line and ADR 0026's "realigned to Node 24", ADR 0027/0029 and the package smoke rewritten for a binary artefact, Prove an OpenCode-derived Crucible shell across Windows, macOS, and Linux #6/Choose Crucible's process, runtime, packaging, and distribution architecture #21 corrected, map row 38 fixed, Run the real-terminal lifecycle suite as a blocking three-OS CI job #56's AC re-pinned and itsready-for-agentreleased, anddocs/research/opencode-engineering-practices.md's "Reject a Bun migration" line revisited.Sandstorm831 commentedon Sep 10, 2026
Resolution
Secant's interactive shell ships as a Bun-compiled single-file executable, not a Node npm package. This overturns the runtime and distribution half of #21 ("Node 24 LTS, one npm package, no self-contained executable in v1"), which rested on #6's disproven "Node needs no FFI" measurement. The decision is taken now; ADR 0030 is written when the Windows soak in #60 passes (a fail on Bun 1.4.2 and again on the next Bun release reverts to the Node 26.4+ npm path with the transparent
--experimental-ffire-exec that #55 landed).Why Bun over Node 26.4: Node's problems are temporary (the flag is a no-op from 26.9, LTS lands 2026-10-28) except the permanent ones — users must install a specific Node, nvm users lose the global on version switch, and a zero-dependency installer is never possible. Three of the four peer CLIs (Claude Code, Codex, OpenCode) ship standalone binaries and Codex wrote down exactly this reason. Bun's costs are one-time build work plus a Windows crash family that is real, open, and managed by pinning (see the findings); the soak measures it before the ADR.
Decisions
packageManager, 1.4.2 or the release Soak the Bun-compiled shell on a real Windows machine before ADR 0030 #60 passes on); CI reads the pin. A Bun bump goes through a PR that passes the three-OS gate and re-arms the human Windows check, exactly as ADR 0027 treats an OpenTUI bump. One toolchain:bun build --compile,bun testwith@opentui/solid/preload,Bun.Terminalfor the real-terminal suite; tsup, tsx, the Babel test loader, and test-onlynode-ptygo;tsc, ESLint, Prettier stay. Baseline x64 builds are moot in Bun 1.4 (AVX2 chosen at runtime).curl | shand PowerShell installers into~/.secant/binon every platform — a fixed location, independent ofSECANT_HOME(ADR 0023), no override variable, matching OpenCode's fixed~/.opencode/bin. An npm launcher@secantdev/secantthat spawns the binary from a per-platformoptionalDependenciespackage with no postinstall (Claude Code's model, so--ignore-scriptsand pnpm cannot break it); Node is needed only to run npm and the shim, any version. No Homebrew, winget, scoop, or Docker in v1. No self-updater and no version check in v1 (unchanged from Choose Crucible's process, runtime, packaging, and distribution architecture #21).codesign --verify --deep --stricton every macOS artefact because Bun's re-signing has broken twice (1.3.12, 1.3.13). No Developer ID, no notarization, no Windows Authenticode in v1.curldoes not set the quarantine flag andtardoes not propagate it, so the curl and npm routes are never Gatekeeper-evaluated; release-page downloads are documented as "run from Terminal, do not double-click". curl-installed Windows executables carry no mark-of-the-web, so SmartScreen does not fire on them (Smart App Control machines excepted).bun:sqlitebehind their Interfaces. Bun 1.4 implementsnode:sqlitebut its Windowsclose()file-lock bug hits the Catalog's open-per-command pattern;bun:sqliteis Bun's first-class API with a transaction helper. Q8 (Choose Crucible's process, runtime, packaging, and distribution architecture #21 §6) closes with this decision: better-sqlite3 is moot under Bun (it needed a Bun fix to load at all and is special-cased by filename).bun:/Bun.are permitted: the SQLite adapter and the Windows console guard. The mechanical check intests/architecture/check-module-boundaries.tsbecomes an allowlist instead of a blanket ban. No Node twin implementations (OpenCode's#sqlite/#pty/#fffnode sides exist only for its Electron app and are untested). Evidence that this is achievable: OpenCode touches Bun APIs in 12 of 833 source files, 25 of its 33 call sites areBun.stringWidth, and it cut from 321 sites in Feb 2026 to 48 by replacing Bun calls with Node builtins.bun:ffiGetConsoleWindow+IsWindowVisible; ConPTY's headless conhost has an invisible window) pointing at Windows Terminal; the TUI still runs and headless is unaffected. Rationale: the wedge is exit-only (a dead console window, no Secant data lost), the exposed population is Windows 10 users launching from a bare cmd/PowerShell window plus elevated shells (upper bound 30 % of desktop Windows, true developer share well below), and OpenCode ships with no handling and eight stale-closed bug reports for it — the notice is what prevents that noise. The human release check retargets from conhost to Windows Terminal, run when the Bun pin, the OpenTUI pin, or the renderer changed. When Bun merges the stdin-release fix (bun#35621) the notice can go.bun-windows-x64on Bun 1.4.2 and run it on a real Windows machine through every launch and exit path in Windows Terminal plus one long session and a 20-launch loop. Conhost is observed and recorded, not deciding.What changes in the record (on #60 pass)
.wfbfiles are embedded in the binary as assets and installed at startup from the embedded bytes; everything else unchanged.node-ptyon Node 24 runners →Bun.Terminalon the pinned Bun), Ship the Windows Terminal human check script and record the M1 report #58 rescoped to the Windows Terminal check plus the M1 report,docs/research/opencode-engineering-practices.md:89("Reject a Bun migration").Future-version ledger
secant upgrade— must write a new file and rename, never overwrite in place (macOS caches the signature in the kernel; in-place overwrite crashes on next launch until reboot).Follow-on tickets
wayfinder:task, HITL, context-ready).bundle buildon the Composition check #52 and Run the real-terminal lifecycle suite as a blocking three-OS CI job #56).Evidence added since the findings comment
$HOME/.opencode/bin(install:68) with no override variable; its data/config/cache/state follow XDG viaxdg-basedir(packages/core/src/global.ts); it has no PowerShell installer (Windows: npm/bun/choco/scoop, WSL recommended).Bun.*sites in 12 of 833 files; threebun/nodeimport-map seams inpackages/core/package.json; the Node side is exercised only by the Electron desktop build (script/build-node.ts) and untested; 321 → 48 sites since Feb 2026 via two deliberate migration campaigns.curl/tardo not set or propagate quarantine (Apple DTS); Bun's ad-hoc re-signing regressed in 1.3.12 (#29120) and 1.3.13 (#32159); Homebrew ended support for casks failing Gatekeeper on 2026-09-01 (brew#20755); in-place binary replacement hazard (Apple).Sandstorm831 commentedon Sep 10, 2026
Resolved above; ADR 0030 and the record amendments follow #60. Follow-ons: #60 (soak, HITL) and #61 (M1 implementation).
docs(#60): author ADR 0030 (Bun-compiled shell) and amend the runtime…