Some research

This commit is contained in:
2026-10-10 09:25:33 +02:00
parent 7add51fd02
commit da51e44359
75 changed files with 2929 additions and 71 deletions
@@ -0,0 +1,58 @@
# Conclusion: Cross-Shell Syntax for the Command-Not-Found and Plexer Hooks (bash/zsh/fish)
## Metadata
| Field | Value |
|---|---|
| Institute | Molodetz Institute — Research & Retrieval Division |
| Subject | shell-multiplexer-auto-attach |
| Document type | Conclusion |
| Tier | N/A |
| Date decided | 2026-10-10 |
| Decided by | retoor, advised by tai (agent, Claude Sonnet 5) |
| Status | Active |
| Confidence | High |
## Question / problem statement
The original `shell-multiplexer-auto-attach` research (2026-10-09) recommended and specified the bash-only command-not-found hook and plexer auto-wrap hook, but explicitly built on bash's own `.bashrc`/`$-`/`command_not_found_handle` primitives throughout. retoor asked for the same user-visible behavior — a bare `tai` command and any unknown command reaching the agent, and a fresh interactive shell transparently dropping into a managed plexer session — to work identically under zsh and fish, not just bash, and asked specifically how to detect each shell and how to avoid any risk of corrupting an existing shell config file while doing it (duplicate blocks, clobbered content, etc.). Deep research into the exact zsh/fish syntax was explicitly requested before writing any shell-specific code.
## Conclusion
**zsh** needs no new syntax at all for the plexer hook: zsh is POSIX-enough that the exact same `[`/`case "$-" in *i*)`/`$TMUX`/`exec` block already used for bash (`PLEXER_BASHRC_BLOCK`) works byte-for-byte unchanged — only the destination file differs (`~/.zshrc` instead of `~/.bashrc`). The only thing that changes for zsh's command-not-found hook is the function name: `command_not_found_handler` (with an "-er" suffix), not bash's `command_not_found_handle`; the body is otherwise identical.
**fish** needs genuinely new syntax throughout, because it shares no syntax primitives with POSIX shells: `status is-interactive` replaces `case "$-" in *i*)`; `test`/`and`-chained conditions replace `[ ] && [ ]`; the `isatty stdin`/`isatty stdout` builtin replaces `[ -t 0 ]`/`[ -t 1 ]` (fish's own `test` builtin has no `-t` file-descriptor primary, confirmed by an explicit, otherwise-inconclusive search); and the command-not-found hook is a `function fish_command_not_found` definition receiving `$argv`, not a POSIX-named handler function. Critically, fish's `config.fish` is sourced on *every* fish startup — interactive or not, login or not, unlike bash's `.bashrc`/zsh's `.zshrc` — so the `status is-interactive` guard inside the fish plexer block is doing real, load-bearing work (not just defense-in-depth the way it is for bash/zsh), confirmed end-to-end by `FishNonInteractiveSkipTest` actually running a plain non-interactive `fish -c` against the installed hook and checking no plexer socket gets created.
The config-corruption risk the user was worried about is handled the same way the original bash-only hooks already handled it, extended rather than reinvented: every block (six total now: notfound × {bash, zsh, fish} and plexer × {bash, zsh, fish}) is wrapped in its own unique begin/end marker pair, upserted via `_rc_block_upsert`/removed via `_rc_block_remove` (shared helpers added in this pass, refactored out of the near-duplicate bash-only functions that already existed), which always back up the target file once (`<file>.bak-tai`) before any write and always strip any stale marked block before re-inserting, making install/reinstall/uninstall idempotent and non-duplicating by construction. A dedicated test (`DualHookSameFileIsolationTest` in `tests/bashrc.py`) proves the notfound and plexer blocks coexist in the same rc file (all three shells put both hooks in one file) without clobbering each other or surrounding user content, for all three shells, deterministically and without needing a real shell.
## Supporting sources
- `../fishshell-docs-4-8-fish-command-not-found.md` (Tier 1) — the `fish_command_not_found` function mechanism and its `$argv` argument shape.
- `../zsh-org-mla-2015-msg00931-command-not-found-handler.md`, `../zsh-org-mla-2009-msg00989-command-not-found-handler.md` (Tier 1) — confirmed the exact zsh function name (`command_not_found_handler`) and that no autoload/registration step is needed beyond the definition.
- `../oracle-docs-e88353-zshbuiltins-autoload-x.md` (Tier 1) — ruled `autoload -X` out as something tai's own (static, non-dynamic) zsh handler needed to use.
- `../fishshell-docs-3-7-isatty.md` (Tier 1) — confirmed fish's `isatty` builtin (and the absence of `test -t`) as the tty-check primitive for the fish plexer block.
- `../fishshell-docs-4-9-status.md` (Tier 1) — confirmed `status is-interactive`/`status is-login` as fish's `$-`-equivalent.
- `../fishshell-blog-release-3-4-0.md` (Tier 1) — an honest negative result on pinning `isatty`'s exact introduction version; didn't block the decision (3.2+ is more than sufficient) but recorded rather than silently dropped.
- `../fishshell-docs-3-4-index-initialization-files.md` (Tier 1) — the single most consequential finding: `config.fish` is always sourced, making the `status is-interactive` guard load-bearing rather than redundant.
- `../github-pyenv-wiki-unix-shell-initialization.md` (Tier 1, secondary/unconfirmed) — corroborating detail on fish's full startup-file ordering; not independently relied on beyond what the official docs already established.
- `../man-archlinux-org-fish-1-en-interactive-flag.md` (Tier 1) — confirmed `fish -i` as a real, stable way to force an interactive fish process, used directly in the new integration tests.
## Alternatives considered and rejected
- **Renaming the existing `bash-hook`/`plexer-hook` install targets to a single shell-agnostic name** (e.g. `shell-hook`) that auto-detects and writes into whichever rc files are present. Rejected in favor of adding explicit `zsh-hook`/`fish-hook`/`zsh-plexer-hook`/`fish-plexer-hook` targets alongside the untouched originals: explicit, narrowly-scoped, independently testable per shell, and zero risk to the existing bash-hook/plexer-hook behavior and tests already relying on those exact names — consistent with "loud and explicit over silently magic."
- **Silently installing into every shell's rc file regardless of whether that shell is present on the machine.** Rejected: writing a dangling hook into `~/.zshrc` on a machine with no zsh installed is exactly the kind of silent, narrow-blast-radius violation this project's own conventions reject. Each new shell-specific install op checks `shutil.which(<shell>)` first and skips loudly (`"zsh-hook: zsh not found, skipped"`) on install/upgrade; uninstall/reinstall still act on the file regardless, so a hook installed before a shell was later removed still gets cleaned up.
- **A from-scratch generic per-shell templating system** (e.g. a single parameterized block description compiled to each shell's syntax). Rejected: fish's syntax is different enough (no `[`/`case`/`$-`, no `-t` test primary) that a shared template would need shell-specific escape hatches for nearly every line anyway, buying no real abstraction over just writing fish's own three blocks directly — "boring technology... fewer moving parts" favored the direct approach actually taken.
## Risks / open questions
The exact fish version that introduced `isatty` was not pinned down (documented since at least 3.2; see `fishshell-blog-release-3-4-0.md`'s negative result) — not a practical risk for any fish a user would realistically have installed today, but noted rather than silently assumed. The `FISH_PLEXER_BLOCK`'s reliance on `status is-interactive` as config.fish's *only* interactivity gate (unlike bash/zsh, where the rc file itself is normally interactive-scoped too) means a future fish release that changed `status is-interactive`'s semantics would be a higher-blast-radius regression for fish than for bash/zsh; this is exactly why `FishNonInteractiveSkipTest` exists as a real, running-fish-binary test rather than a unit test of the file content alone.
## Action taken
Implemented in full in the same session as this research, not deferred: `tai.py` gained `shell_rc_path()`/`shell_present()`, the `ZSH_NOTFOUND_BLOCK`/`FISH_NOTFOUND_BLOCK`/`FISH_PLEXER_BLOCK` constants, the shared `_rc_block_present`/`_rc_block_remove`/`_rc_block_upsert` helpers (the existing bash-only functions were refactored to delegate to these rather than duplicating the logic a third and fourth time), the per-shell `zsh_hook_present`/`upsert_zsh_hook_block`/`remove_zsh_hook_block` family (and fish/plexer equivalents), the shared `op_shell_hook()` plus `op_zsh_hook`/`op_fish_hook`/`op_zsh_plexer_hook`/`op_fish_plexer_hook`, four new `INSTALL_TARGETS` entries, updated `status_lines()`/`install_report()`/the `install` tool's description, and `install_self()` now additionally detects `$SHELL` and installs the matching shell's hook. Tests: new unit tests in `test_tai.py` (`InstallOpsTests`, `InstallTests`) covering install/uninstall/idempotency/skip-when-missing/XDG_CONFIG_HOME/directory-creation for both new shells, all passing alongside the full existing 226-test suite (237 after additions); new real-shell integration tests in `tests/bashrc.py` (`ZshInstallAndAutoWrapTest`, `ZshAntiRecursionTest`, `ZshTmuxActiveSkipTest`, `ZshNonInteractiveSkipTest`, and the fish equivalents, plus `DualHookSameFileIsolationTest`), gated the same way the existing bash tests are (`TAI_CONTAINER_TEST=1`-only); `tests/docker/Dockerfile` now installs `zsh` and `fish` alongside `bash` so those real-shell tests actually run in the disposable container rather than only existing as aspirational code. `README.md`'s Installation section was rewritten to document all eleven install targets accurately (it previously undercounted them and didn't mention `plexer-hook` at all, a pre-existing documentation gap fixed in passing since this edit touched that exact paragraph).
## Changelog
| Date | Change |
|---|---|
| 2026-10-10 | Initial conclusion recorded. |