diff --git a/README.md b/README.md index 1c9d355..7cd3e25 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,20 @@ Any other input is sent to the agent. /install status /install upgrade scheduler-service -Six install targets exist side by side: `binary` (`~/.local/bin/tai.py`), `bash-hook` (a guarded block in `~/.bashrc`, backed up once to `~/.bashrc.bak-tai`, adding both an `alias tai="$HOME/.local/bin/tai.py"` so the bare `tai` command resolves to the REPL instead of falling through, and a `command_not_found_handle` so any other unknown shell command is answered by the agent), `venv` (`~/.tai/venv`, created whenever the venv module exists), `scheduler-service` and `telegram-service` (systemd user units that prefer the venv python), and `container` (the sandbox, whose image carries its own `/box/venv`). `/install status` (or the `install` tool) reports exactly which of these exist, with versions and service states. `install` adds missing pieces, `upgrade` refreshes in place, `reinstall` rebuilds artifacts from scratch, `uninstall` removes them; service changes restart or start units immediately. Data handling is explicit: no action ever touches the vault (`memory.db`, secrets, schedules, records, backups). +Eleven install targets exist side by side: `binary` (`~/.local/bin/tai.py`); a command-not-found hook per shell -- `bash-hook` (`~/.bashrc`), `zsh-hook` (`~/.zshrc`), `fish-hook` (`~/.config/fish/config.fish`, respecting `$XDG_CONFIG_HOME`) -- each a guarded block (backed up once to `.bak-tai` before the first write) adding a `tai` alias/function so the bare `tai` command resolves to the REPL instead of falling through, plus the shell's own not-found mechanism (`command_not_found_handle` for bash, `command_not_found_handler` for zsh, a `fish_command_not_found` function for fish) so any other unknown shell command is answered by the agent instead; a plexer auto-wrap hook per shell -- `plexer-hook` (bash), `zsh-plexer-hook`, `fish-plexer-hook` -- that transparently re-execs a fresh interactive shell into a managed, scriptable plexer session (`tai.py --plexer-wrap`; skipped automatically inside tmux, inside another plexer-wrapped shell, or when `~/.tai-no-plexer` exists) using whatever that shell can actually express (`[`/`case "$-"` for bash and zsh -- byte-identical content, since both are POSIX enough for it; `status is-interactive`/`test`/`isatty` for fish, which has neither); `venv` (`~/.tai/venv`, created whenever the venv module exists); `scheduler-service` and `telegram-service` (systemd user units that prefer the venv python); and `container` (the sandbox, whose image carries its own `/box/venv`). Installing a zsh/fish-specific target on a machine without that shell skips loudly (`"zsh-hook: zsh not found, skipped"`) rather than writing a dangling hook into a config file nothing will ever source; `uninstall`/`reinstall` still act on the file regardless, so a hook installed before a shell was later removed still gets cleaned up. `/install status` (or the `install` tool) reports exactly which of these exist, with versions and service states. `install` adds missing pieces, `upgrade` refreshes in place, `reinstall` rebuilds artifacts from scratch, `uninstall` removes them; service changes restart or start units immediately. Data handling is explicit: no action ever touches the vault (`memory.db`, secrets, schedules, records, backups). `./tai.py --install` (the one-shot CLI bootstrap) always installs the bash hook and, in addition, detects `$SHELL` and installs that shell's matching hook too when it's zsh or fish. + +## Plexer + +A plexer session is a terminal tai can see into and type into, not just read a one-shot command's output from. There are two ways one gets active, and tai never cares which: + +- **tmux** — if the shell is already inside a tmux pane (`$TMUX_PANE` set), that pane *is* the session. tai drives it with plain `tmux send-keys`/`capture-pane`/`list-panes`; nothing is spawned. +- **native wrapper** — if nothing already owns the terminal, the plexer-hook/`zsh-plexer-hook`/`fish-plexer-hook` installed above re-execs the fresh interactive shell into `tai.py --plexer-wrap`, which forks the user's real shell onto a pty (`NativePlexerBackend`) and relays bytes between the real terminal and that pty in both directions, forwarding resize (`SIGWINCH`), teeing everything to a log file under `~/.tai/plexer/`, and serving control commands over a Unix socket (`TAI_PLEXER_SOCKET`) so a separate tai process — the agent the user is actually talking to — can drive the same session from outside. + +Either way, four tools appear only while a session is actually active (`plexer_send`, `plexer_read`, `plexer_type`, `plexer_status` — hidden otherwise, not just disabled, via `TOOL_ENV_GATES`): `plexer_send` types literal keystrokes (optionally pressing Enter), `plexer_read` reads back the last N lines of real rendered output, `plexer_type` does the same as `send` but one character at a time with a delay for a visible typing effect, and `plexer_status` reports which backend is active and its details (pid/socket/log, or tmux pane id). Which backend answers a given call is autodetected from the environment (`TAI_PLEXER_SOCKET` vs `$TMUX_PANE`), never configured by hand. + +Inside a natively-wrapped session, Ctrl+A (deliberately not tmux's Ctrl+B, so the two never collide if one is somehow nested in the other) is reserved as tai's own prefix key; `Ctrl+A q` kills the wrapped shell outright (`SIGKILL` to its process group — this is a clean kill, not a tmux-style detach-and-reattach, since there is no session persistence to reattach to). Everything else typed passes through untouched. + +The auto-wrap checks three conditions, in the shell hook itself, before ever invoking `tai.py --plexer-wrap`: never wrap if a plexer is already active (`$TAI_PLEXER_ACTIVE`), never wrap if tmux already is (`$TMUX`), and never wrap if `~/.tai-no-plexer` exists — the hand brake. That file is the one thing to touch if the auto-wrap is ever doing the wrong thing on a given machine; it needs no flag or argument, just existing is enough, and it's rechecked on the Python side too (`plexer_should_wrap()`) as defense in depth, not trusted to the shell guard alone. ## Backends diff --git a/research/INDEX.md b/research/INDEX.md index 38fbc65..5087a7e 100644 --- a/research/INDEX.md +++ b/research/INDEX.md @@ -7,12 +7,12 @@ | Metric | Value | |---|---| | Subjects | 4 | -| Total source documents | 225 | -| — Tier 1 (Source Dossier, independently analyzed) | 84 | -| — Tier 2 (Source Citation Card, listed only) | 141 | -| Total conclusion documents | 4 | +| Total source documents | 292 | +| — Tier 1 (Source Dossier, independently analyzed) | 94 | +| — Tier 2 (Source Citation Card, listed only) | 198 | +| Total conclusion documents | 5 | | Archive opened | 2026-10-09 | -| Last updated | 2026-10-09 | +| Last updated | 2026-10-10 | ## Subjects @@ -21,7 +21,7 @@ | [`sqlite-rag-hybrid-search`](sqlite-rag-hybrid-search/) | 32 (10 / 22) | 1 | 2026-10-09 | 2026-10-09 | Designing tmux-log export + chunked retrieval; settled on BM25-only, no vector DB | | [`agent-memory-recency-tracking`](agent-memory-recency-tracking/) | 63 (16 / 47) | 1 | 2026-10-09 | 2026-10-09 | Designing an LRU-style last-used/use-count tracker for vault records and chunks | | [`async-pty-process-testing`](async-pty-process-testing/) | 59 (20 / 39) | 1 | 2026-10-09 | 2026-10-09 | Designing tai_test.py (pty vs. pipes, non-blocking stdin, process-group cleanup, test-framework shape) — backfilled after it was initially skipped, see activity log | -| [`shell-multiplexer-auto-attach`](shell-multiplexer-auto-attach/) | 71 (38 / 33) | 1 | 2026-10-09 | 2026-10-09 | Designing a tai-managed terminal multiplexer + bashrc auto-attach: tmux-as-substrate recommendation, interactive-vs-automation shell detection | +| [`shell-multiplexer-auto-attach`](shell-multiplexer-auto-attach/) | 138 (48 / 90) | 2 | 2026-10-09 | 2026-10-10 | Designing a tai-managed terminal multiplexer + bashrc auto-attach: tmux-as-substrate recommendation, interactive-vs-automation shell detection; extended 2026-10-10 to cover zsh/fish hook syntax | ## Recent activity log @@ -33,6 +33,7 @@ | 2026-10-09 | `agent-memory-recency-tracking` | 63 sources (16 Tier 1 / 47 Tier 2) + 1 conclusion recorded (the conclusion: `last_used`/`use_count` tracking proposal for records and chunks, throttled-write pattern, no eviction policy yet — see `conclusions/`). One source (`glama-code-index-mcp-bm25-hybrid.md`) was promoted from the originally-planned Tier 2 to Tier 1 by the authoring agent mid-task because it contained specific attributable numeric findings — exactly the judgment call §3 of `README.md` calls for, logged here rather than silently absorbed. Counts verified directly against the written files. | | 2026-10-09 | `async-pty-process-testing` | New subject, opened as a correction: the eight `WebSearch` calls behind `tai_test.py`'s design (pty vs. pipes, non-blocking stdin, process-group cleanup, test-framework patterns) were initially skipped from the archive under the reasoning "informal engineering reference, not a design decision" — a silent, unilateral exception to the Research Rule, and wrong (see `README.md` §4's added "no exception, ever" paragraph). Backfilled: 59 sources (20 Tier 1 / 39 Tier 2) + 1 conclusion (`conclusions/tai-test-framework-design.md`). Counts verified directly against the written files via `research/audit.py`, not taken from the authoring agent's self-report alone. A `PostToolUse`/`WebSearch` hook (`.claude/settings.json`) was also added the same day specifically to stop this from recurring. | | 2026-10-09 | `shell-multiplexer-auto-attach` | 71 sources (38 Tier 1 / 33 Tier 2) + 1 conclusion recorded (the conclusion: build on tmux as a substrate rather than a from-scratch multiplexer; reliable human-vs-automation shell detection has no single solution, only a combination of weaker signals plus a safety-file escape hatch). Counts verified directly against the written files via `research/audit.py`. Hook fired correctly for all 8 underlying WebSearch calls during this task. | +| 2026-10-10 | `shell-multiplexer-auto-attach` | Extended with 67 more sources (10 Tier 1 / 57 Tier 2) + 1 more conclusion (`conclusions/cross-shell-hook-syntax-bash-zsh-fish.md`), covering 7 WebSearch calls on zsh/fish command-not-found and plexer-hook syntax (fish_command_not_found, zsh command_not_found_handler, fish's isatty builtin vs. a missing `test -t`, `status is-interactive`/`is-login`, config.fish always being sourced, and the `fish -i` flag). Subject total now 138 sources (48 Tier 1 / 90 Tier 2), 2 conclusions. The research backed a same-session implementation (tai.py's `shell_rc_path`/`ZSH_NOTFOUND_BLOCK`/`FISH_NOTFOUND_BLOCK`/`FISH_PLEXER_BLOCK`/`op_zsh_hook`/`op_fish_hook`/`op_zsh_plexer_hook`/`op_fish_plexer_hook`, new tests in `test_tai.py` and `tests/bashrc.py`, and `zsh`/`fish` added to `tests/docker/Dockerfile`) rather than remaining a proposal. Counts verified directly against the written files via `research/audit.py` (PASS). | ## How to use this index diff --git a/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-dev-v3-16-ppc64le.md b/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-dev-v3-16-ppc64le.md new file mode 100644 index 0000000..3657030 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-dev-v3-16-ppc64le.md @@ -0,0 +1,28 @@ +# Alpine Linux fish-dev package page (v3.16, ppc64le) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Package details | +| Source URL | https://pkgs.alpinelinux.org/package/v3.16/main/ppc64le/fish-dev | +| Source type | other | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another architecture's Alpine package-index entry for the same fish version; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-dev-v3-16-s390x.md b/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-dev-v3-16-s390x.md new file mode 100644 index 0000000..a09ae73 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-dev-v3-16-s390x.md @@ -0,0 +1,28 @@ +# Alpine Linux fish-dev package page (v3.16, s390x) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Package details | +| Source URL | https://pkgs.alpinelinux.org/package/v3.16/main/s390x/fish-dev | +| Source type | other | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another architecture's Alpine package-index entry for the same fish version; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-v3-16-x86.md b/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-v3-16-x86.md new file mode 100644 index 0000000..e0e81b9 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/alpinelinux-pkgs-fish-v3-16-x86.md @@ -0,0 +1,28 @@ +# Alpine Linux fish package page (v3.16, x86) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Package details | +| Source URL | https://pkgs.alpinelinux.org/package/v3.16/main/x86/fish | +| Source type | other | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A distro package-index page for fish 3.16-era Alpine builds, surfaced while trying to date isatty's introduction; does not itself state a changelog entry, not independently read in depth. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/conclusions/cross-shell-hook-syntax-bash-zsh-fish.md b/research/shell-multiplexer-auto-attach/conclusions/cross-shell-hook-syntax-bash-zsh-fish.md new file mode 100644 index 0000000..fa68374 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/conclusions/cross-shell-hook-syntax-bash-zsh-fish.md @@ -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 (`.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()` 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. | diff --git a/research/shell-multiplexer-auto-attach/debian-dyn-manpages-trixie-fish-1.md b/research/shell-multiplexer-auto-attach/debian-dyn-manpages-trixie-fish-1.md new file mode 100644 index 0000000..a63e89b --- /dev/null +++ b/research/shell-multiplexer-auto-attach/debian-dyn-manpages-trixie-fish-1.md @@ -0,0 +1,28 @@ +# fish(1) manpage, Debian trixie dyn-mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://dyn.manpages.debian.org/trixie/fish-common/fish.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A second Debian manpage-archive path (dyn.manpages.debian.org) for the same trixie fish(1) reference; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/debian-manpages-trixie-fish-1.md b/research/shell-multiplexer-auto-attach/debian-manpages-trixie-fish-1.md new file mode 100644 index 0000000..3ba56c9 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/debian-manpages-trixie-fish-1.md @@ -0,0 +1,28 @@ +# fish(1) manpage, Debian trixie mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://manpages.debian.org/trixie/fish-common/fish.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A Debian manpage-archive mirror of the fish(1) reference, matching the current wording; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/debian-manpages-unstable-fish-doc-1.md b/research/shell-multiplexer-auto-attach/debian-manpages-unstable-fish-doc-1.md new file mode 100644 index 0000000..7b9bbb2 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/debian-manpages-unstable-fish-doc-1.md @@ -0,0 +1,28 @@ +# fish-doc(1) manpage, Debian unstable mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://dyn.manpages.debian.org/unstable/fish-common/fish-doc.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A Debian manpage-archive mirror of the fish-doc(1) reference; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-blog-release-3-4-0.md b/research/shell-multiplexer-auto-attach/fishshell-blog-release-3-4-0.md new file mode 100644 index 0000000..6597d54 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-blog-release-3-4-0.md @@ -0,0 +1,48 @@ +# fish 3.4.0 release notes + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | fish 3.4.0 release blog post | +| Source URL | https://fishshell.com/blog/release-3-4-0 | +| Source publisher / author | fish-shell project (fishshell.com) | +| Source type | blog post | +| Source date (published) | Unknown — not stated (fish 3.4.0's own release date) | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A dedicated search attempting to pin down exactly which fish version introduced the `isatty` builtin (prompted by it being documented as early as the 3.2 doc mirror, to check whether that's actually when it shipped or just the earliest mirror checked) came back inconclusive. This is the headline release post for fish 3.4.0, included here specifically as negative evidence: it highlights command substitution syntax, a theme-changing terminal interface, faster globbing, and disabling autosuggestions, but does not mention `isatty` at all. + +## Key findings + +- fish 3.4.0's release notes do not mention `isatty`, which is consistent with `isatty` having shipped *before* 3.4.0 (matching the already-documented presence in the 3.2 doc mirror) rather than being introduced by it. +- No changelog entry pinning the exact introduction version of `isatty` was found by this search; the project's fish-shell `CHANGELOG.rst` (not fetched) is identified as the next place to check if the exact version ever becomes load-bearing. +- This negative result is recorded honestly rather than silently dropped, per this archive's own quality bar on confidence ratings — "isatty exists by 3.2, introduction date not confirmed further back" is the accurate state of knowledge, not "isatty was introduced in 3.2" (which was never actually confirmed, only observed as the earliest doc mirror available). + +## Pros / strengths + +- An honest, verifiable negative finding (absence of a mention) rather than a vague "couldn't find it" — a reader can independently check the same release notes and see the same absence. + +## Cons / limitations / caveats + +- Doesn't answer the question it was sought for; the actual introduction version of `isatty` remains unconfirmed beyond "documented by 3.2" (see fishshell-docs-3-7-isatty.md). + +## Relevance to tai + +Not directly load-bearing for tai's implementation (which only needs `isatty` to exist in any fish worth supporting today, confirmed since 3.2), but recorded so this project doesn't silently pretend the exact introduction version was confirmed when it was not. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-1-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-1-status.md new file mode 100644 index 0000000..e790b1e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-1-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 3.1) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/3.1/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-2-fish-command-not-found.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-2-fish-command-not-found.md new file mode 100644 index 0000000..e9077b4 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-2-fish-command-not-found.md @@ -0,0 +1,28 @@ +# fish_command_not_found docs (fish 3.2) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fish_command_not_found - what to do when a command wasn't found | +| Source URL | https://fishshell.com/docs/3.2/cmds/fish_command_not_found.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the fish_command_not_found query as an older-version mirror of the same doc page; the 4.8 version (fishshell-docs-4-8-fish-command-not-found.md, Tier 1) was read in depth instead, confirming the feature's introduction version. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-2-isatty.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-2-isatty.md new file mode 100644 index 0000000..5d5772d --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-2-isatty.md @@ -0,0 +1,28 @@ +# isatty builtin docs (fish 3.2) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty - test if a file descriptor is a terminal | +| Source URL | https://fishshell.com/docs/3.2/cmds/isatty.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An older-version mirror of the isatty builtin doc page; the 3.7 version (fishshell-docs-3-7-isatty.md, Tier 1) was read in depth instead. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-fish-command-not-found.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-fish-command-not-found.md new file mode 100644 index 0000000..7c1f219 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-fish-command-not-found.md @@ -0,0 +1,28 @@ +# fish_command_not_found docs (fish 3.3) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fish_command_not_found - what to do when a command wasn't found | +| Source URL | https://fishshell.com/docs/3.3/cmds/fish_command_not_found.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the same fish_command_not_found doc page, surfaced by the same query; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-index-initialization-files.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-index-initialization-files.md new file mode 100644 index 0000000..35ca3b3 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-index-initialization-files.md @@ -0,0 +1,28 @@ +# fish docs index, fish 3.3 (initialization files section) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fishshell.com | +| Source URL | https://fishshell.com/docs/3.3/index.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An older-version mirror of the fish docs index page covering initialization files (same section read in depth on the 3.4 index, Tier 1, at fishshell-docs-3-4-index-initialization-files.md); also reachable at the bare https://fishshell.com/docs/3.3 URL. Not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-isatty.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-isatty.md new file mode 100644 index 0000000..9c1f704 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-isatty.md @@ -0,0 +1,28 @@ +# isatty builtin docs (fish 3.3) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty - test if a file descriptor is a terminal | +| Source URL | https://fishshell.com/docs/3.3/cmds/isatty.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the isatty builtin doc page, used only to confirm cross-version stability in the Tier 1 dossier; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-status.md new file mode 100644 index 0000000..161214f --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-3-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 3.3) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/3.3/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-fish-command-not-found.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-fish-command-not-found.md new file mode 100644 index 0000000..feda307 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-fish-command-not-found.md @@ -0,0 +1,28 @@ +# fish_command_not_found docs (fish 3.4) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fish_command_not_found - what to do when a command wasn't found | +| Source URL | https://fishshell.com/docs/3.4/cmds/fish_command_not_found.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the same fish_command_not_found doc page, surfaced by the same query; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-index-initialization-files.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-index-initialization-files.md new file mode 100644 index 0000000..510ca92 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-index-initialization-files.md @@ -0,0 +1,48 @@ +# fish docs index: initialization files (config.fish is always sourced) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | fish documentation index (3.4) | +| Source URL | https://fishshell.com/docs/3.4/index.html | +| Source publisher / author | fish-shell project (fishshell.com) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +fish's official docs state plainly that its startup files are read "on the startup of every shell, not just interactive ones" — unlike bash's `.bashrc` (interactive-only, non-login) or `.profile`/`.bash_profile` (login-only). `~/.config/fish/config.fish` (respecting `$XDG_CONFIG_HOME`) is read on *every* fish invocation, interactive or not, login or not, and is explicitly the right place to put code that should run unconditionally — with `status is-interactive`/`status is-login` as the documented way to scope behavior within it. Snippets in `~/.config/fish/conf.d/` run before `config.fish`, which runs last so it can override them. + +## Key findings + +- This is the single most consequential difference from bash/zsh for this project: `.bashrc` and `.zshrc` are themselves gated to interactive shells by the shell's own startup logic (roughly), so the bash/zsh plexer block's `case "$-" in *i*)` guard is a belt-and-suspenders check on top of a file that mostly only gets read in the right context anyway. `config.fish` has no such built-in gating — it runs unconditionally, so the `status is-interactive` guard inside `FISH_PLEXER_BLOCK` is doing *all* of the interactivity-scoping work, not just reinforcing it. +- The fish FAQ (per the search tool's synthesis) explicitly calls out this exact contrast with `.bashrc`/`.profile`, confirming it's a documented, intentional design difference in fish, not an edge case this research happened to stumble on. +- `~/.config/fish/config.fish`'s location follows `$XDG_CONFIG_HOME` — directly informing `shell_rc_path()` in tai.py, which checks `os.environ.get("XDG_CONFIG_HOME")` before falling back to `~/.config`. + +## Pros / strengths + +- Official documentation stating the behavior plainly and explicitly contrasting it with bash/zsh conventions, removing any ambiguity about whether the interactivity guard is optional. + +## Cons / limitations / caveats + +- A third-party pyenv wiki page (github-pyenv-wiki-unix-shell-initialization.md, also Tier 1) lists a fuller startup-file ordering (system/user `conf.d`, `vendor_conf.d`, `/etc/fish/config.fish`, then `~/.config/fish/config.fish`) that this search could not independently confirm against the official docs; treated as secondary/unconfirmed per that dossier's own caveat, not relied on for anything load-bearing here. + +## Relevance to tai + +This is the finding that made the `status is-interactive` guard non-optional in `FISH_PLEXER_BLOCK`: without it, installing the plexer hook into `config.fish` would attempt to `exec tai.py --plexer-wrap` on *every* fish invocation, including scripts, `fish -c`, and tooling that shells out to fish non-interactively — exactly the failure mode the user explicitly wanted to avoid. Confirmed end-to-end by the `FishNonInteractiveSkipTest` integration test in `tests/bashrc.py`, which runs a plain `fish -c 'echo marker'` (no explicit sourcing needed, since config.fish always runs) and asserts no plexer socket gets created. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-isatty.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-isatty.md new file mode 100644 index 0000000..cf0d01f --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-isatty.md @@ -0,0 +1,28 @@ +# isatty builtin docs (fish 3.4) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty - test if a file descriptor is a terminal | +| Source URL | https://fishshell.com/docs/3.4/cmds/isatty.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the isatty builtin doc page, used only to confirm cross-version stability in the Tier 1 dossier; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-status.md new file mode 100644 index 0000000..a360491 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-4-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 3.4) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/3.4/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-5-isatty.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-5-isatty.md new file mode 100644 index 0000000..50c9c55 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-5-isatty.md @@ -0,0 +1,28 @@ +# isatty builtin docs (fish 3.5) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty - test if a file descriptor is a terminal | +| Source URL | https://fishshell.com/docs/3.5/cmds/isatty.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the isatty builtin doc page, used only to confirm cross-version stability in the Tier 1 dossier; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-fish-command-not-found.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-fish-command-not-found.md new file mode 100644 index 0000000..3687d17 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-fish-command-not-found.md @@ -0,0 +1,28 @@ +# fish_command_not_found docs (fish 3.6) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fish_command_not_found - what to do when a command wasn't found | +| Source URL | https://fishshell.com/docs/3.6/cmds/fish_command_not_found.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the same fish_command_not_found doc page, surfaced by the same query; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-isatty.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-isatty.md new file mode 100644 index 0000000..f252a8f --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-isatty.md @@ -0,0 +1,28 @@ +# isatty builtin docs (fish 3.6) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty - test if a file descriptor is a terminal | +| Source URL | https://fishshell.com/docs/3.6/cmds/isatty.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the isatty builtin doc page, used only to confirm cross-version stability in the Tier 1 dossier; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-status.md new file mode 100644 index 0000000..7323c12 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-6-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 3.6) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/3.6/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-3-7-isatty.md b/research/shell-multiplexer-auto-attach/fishshell-docs-3-7-isatty.md new file mode 100644 index 0000000..7714a8a --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-3-7-isatty.md @@ -0,0 +1,49 @@ +# isatty - test if a file descriptor is a terminal + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | isatty - test if a file descriptor is a terminal | +| Source URL | https://fishshell.com/docs/3.7/cmds/isatty.html | +| Source publisher / author | fish-shell project (fishshell.com) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +fish ships `isatty` as its own standalone builtin command (distinct from a `test -t` primary), accepting either a numeric file descriptor or one of the names `stdin`/`stdout`/`stderr`, defaulting to fd 0 if no argument is given, and returning exit status 0 when the descriptor is a terminal. A dedicated search for whether fish's `test` builtin itself supports a `-t` file-descriptor primary (the POSIX `test -t` equivalent) came back empty across every doc mirror checked — `isatty` is the only confirmed mechanism for this check in fish. + +## Key findings + +- `isatty` takes a named stream (`stdin`, `stdout`, `stderr`) or a raw fd number; `isatty stdin`/`isatty stdout` is the idiomatic way to check both streams, directly mirroring what the bash/zsh plexer block does with `[ -t 0 ] && [ -t 1 ]`. +- Exit status 0 means "is a terminal", non-zero means "is not" — same polarity as POSIX `test -t`, so no inverted-logic trap when porting the bash/zsh condition. +- The same behavior is documented identically across every fish doc version mirror checked from 3.2 through 4.5 (the 3.2, 3.3, 3.4, 3.5, 3.6 version pages and the w3cub 4.5 mirror all state the same thing), so this is a long-stable, safe builtin to depend on rather than a recent or version-fragile addition. +- fish's `test` builtin was separately confirmed (via the companion search for a changelog/version-introduction date) to not document a `-t` primary at all in any version checked; `isatty` is therefore not an optional shortcut but the only available mechanism for this check in fish. + +## Pros / strengths + +- Officially documented, stable across at least five minor versions (3.2–3.6, plus 4.5 via mirror) with word-for-word identical behavior description — a safe primitive to build the plexer hook's tty check on. + +## Cons / limitations / caveats + +- Exact version of introduction could not be pinned down (see the separate, explicitly inconclusive isatty-version-changelog search, Tier 1 dossier fishshell-blog-release-3-4-0.md); documented as early as 3.2 in this search, which is more than sufficient for this project's purposes (any fish worth supporting today postdates 3.2 by years) but is a documented gap, not a guess. + +## Relevance to tai + +Directly the basis for `FISH_PLEXER_BLOCK` in tai.py using `isatty stdin` and `isatty stdout` instead of attempting a `test -t 0`/`test -t 1` primary that fish's `test` builtin does not support. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-4-0-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-4-0-status.md new file mode 100644 index 0000000..60b4a4e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-4-0-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 4.0) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/4.0/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A version mirror of the fish status command doc page; the 4.9 version (fishshell-docs-4-9-status.md, Tier 1) was read in depth instead, confirming is-interactive/is-login are stable across versions. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-4-1-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-4-1-status.md new file mode 100644 index 0000000..7b3b783 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-4-1-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 4.1) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/4.1/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-4-2-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-4-2-status.md new file mode 100644 index 0000000..4faa7e5 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-4-2-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 4.2) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/4.2/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-4-3-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-4-3-status.md new file mode 100644 index 0000000..31176e8 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-4-3-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 4.3) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/4.3/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-4-4-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-4-4-status.md new file mode 100644 index 0000000..6612d93 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-4-4-status.md @@ -0,0 +1,28 @@ +# status command docs (fish 4.4) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/4.4/cmds/status.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another version mirror of the fish status command doc page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-4-8-fish-command-not-found.md b/research/shell-multiplexer-auto-attach/fishshell-docs-4-8-fish-command-not-found.md new file mode 100644 index 0000000..2a23b38 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-4-8-fish-command-not-found.md @@ -0,0 +1,50 @@ +# fish_command_not_found - what to do when a command wasn't found + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | fish_command_not_found - what to do when a command wasn't found | +| Source URL | https://fishshell.com/docs/4.8/cmds/fish_command_not_found.html | +| Source publisher / author | fish-shell project (fishshell.com) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +fish's analogue of bash's `command_not_found_handle`/zsh's `command_not_found_handler` is a plain user-definable function named `fish_command_not_found`, called whenever fish fails to find a command. It receives the attempted command line as `$argv` (one token per arg, so `$argv[1]` is the missing command name), was introduced in fish 3.2.0, and falls back to fish's own default error message if left undefined or if the function explicitly calls `__fish_default_command_not_found_handler`. + +## Key findings + +- Defined with ordinary fish function syntax: `function fish_command_not_found; ...; end`, no special registration step required beyond defining the function (typically in `config.fish` or an autoloaded `~/.config/fish/functions/fish_command_not_found.fish` file). +- `$argv` holds the full attempted command line as separate tokens, directly analogous to bash's `"$@"` inside `command_not_found_handle`. +- Introduced in fish 3.2.0; versions before that used a `fish_command_not_found` *event* instead of a function, so a handler meant to support pre-3.2 fish needs a dual event+function definition — not a concern for this project, which only needs to support a function since its own `tai.py` requires modern Python 3 anyway and fish 3.2 (2020) predates any realistic target. +- Leaving the function undefined, or having it call `__fish_default_command_not_found_handler`, reproduces fish's normal plain error message — useful as the explicit fallback behavior if tai's own hook is ever uninstalled. + +## Pros / strengths + +- Official, current vendor documentation, consistent across every mirrored version from 3.2 through 4.x found in this search — a stable, non-moving target to build on. +- Directly structurally parallel to bash/zsh's own mechanism (one function, one argv), which kept the tai.py implementation simple: same body shape (`"$HOME/.local/bin/tai.py" $argv`) as the bash/zsh blocks, just fish function syntax instead of a shell function definition. + +## Cons / limitations / caveats + +- Unlike bash's `command_not_found_handle`/zsh's `command_not_found_handler`, which are ordinary shell functions sourced from any rc file read at the right time, fish's function must specifically be named `fish_command_not_found` — fish does not look for an alternately-named function for this purpose. + +## Relevance to tai + +This is the exact mechanism `FISH_NOTFOUND_BLOCK` in `tai.py` implements: a `function fish_command_not_found; "$HOME/.local/bin/tai.py" $argv; end` block written into `config.fish` by `upsert_fish_hook_block`/`op_fish_hook`, directly parallel to the existing bash/zsh blocks. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/fishshell-docs-4-9-status.md b/research/shell-multiplexer-auto-attach/fishshell-docs-4-9-status.md new file mode 100644 index 0000000..c525e49 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fishshell-docs-4-9-status.md @@ -0,0 +1,48 @@ +# status - query fish runtime information + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | status - query fish runtime information | +| Source URL | https://fishshell.com/docs/4.9/cmds/status.html | +| Source publisher / author | fish-shell project (fishshell.com) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +fish has no `$-`-style variable at all; the equivalent of bash/zsh's `case "$-" in *i*)` interactivity check is the `status is-interactive` subcommand (short form `status -i` / `--is-interactive`), which succeeds (exit 0) when fish is connected to a keyboard — i.e. actually interactive, independent of how it was started. A separate `status is-login` (`-l`/`--is-login`) checks login-shell status. Both subcommands are documented identically across every version mirror checked, from fish 3.1 through 4.9. + +## Key findings + +- `status is-interactive` is the single, direct fish replacement for the bash/zsh `case "$-" in *i*)` guard used in `PLEXER_BASHRC_BLOCK` — same semantic question ("is this shell actually interactive"), different syntax entirely. +- `status is-login` exists as a separate, distinct check — not needed by tai's plexer hook (which only cares about interactivity, not login status) but confirms fish does expose that distinction too, the same way bash's `shopt login_shell` does. +- Behavior is stable and identically worded across fish 3.1, 3.3, 3.4, 3.6, 4.0, 4.1, 4.2, 4.3, 4.4, and 4.9 doc mirrors — this is a long-established, non-fragile part of fish's interface. + +## Pros / strengths + +- Official vendor documentation, cross-checked across ten version mirrors with word-for-word consistent behavior — as safe a primitive as `isatty` to build on. + +## Cons / limitations / caveats + +- Unlike `$-`, which is a variable any shell code can inspect without a subcommand call, `status is-interactive` is a command invocation; this is a fish idiom difference, not a limitation, but worth noting since it changes how the guard composes with other conditions (fish's `if cond1; and cond2; and cond3` chaining, rather than a single `&&`-joined test expression). + +## Relevance to tai + +Directly the basis for the `status is-interactive` guard at the top of `FISH_PLEXER_BLOCK` in tai.py — the single most load-bearing line in that block, since `config.fish` (unlike `.bashrc`/`.zshrc`) is sourced on every fish startup regardless of interactivity (see fishshell-docs-3-4-index-initialization-files.md), making this guard do work that bash/zsh get for free from their rc files only being read for interactive shells in the first place. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/git-charnock-void-packages-fish-commit-3d6c827.md b/research/shell-multiplexer-auto-attach/git-charnock-void-packages-fish-commit-3d6c827.md new file mode 100644 index 0000000..bfdaa37 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/git-charnock-void-packages-fish-commit-3d6c827.md @@ -0,0 +1,28 @@ +# void-packages fish bump commit (charnock mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | git.charnock.fr | +| Source URL | https://git.charnock.fr/tomcharnock/void-packages/commit/3d6c82797ac59cb517f649c52945109821540f98 | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A Void Linux packaging commit bumping the fish package version, surfaced while trying to date isatty's introduction; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/git-charnock-void-packages-fish-commit-586b1f2-patch.md b/research/shell-multiplexer-auto-attach/git-charnock-void-packages-fish-commit-586b1f2-patch.md new file mode 100644 index 0000000..d53d641 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/git-charnock-void-packages-fish-commit-586b1f2-patch.md @@ -0,0 +1,28 @@ +# void-packages fish bump commit, patch view (charnock mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | git.charnock.fr | +| Source URL | https://git.charnock.fr/tomcharnock/void-packages/commit/586b1f290d27273ae6eddc4e99579b68ac8b03e8.patch | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +The raw .patch view of another Void Linux fish packaging commit; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/gitdab-cynosphere-dotfiles-command-not-found-commit-patch.md b/research/shell-multiplexer-auto-attach/gitdab-cynosphere-dotfiles-command-not-found-commit-patch.md new file mode 100644 index 0000000..56aebac --- /dev/null +++ b/research/shell-multiplexer-auto-attach/gitdab-cynosphere-dotfiles-command-not-found-commit-patch.md @@ -0,0 +1,28 @@ +# Cynosphere dotfiles commit, patch view + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | gitdab.com | +| Source URL | https://gitdab.com/Cynosphere/dotfiles-pub/commit/24e04853691cd564f2c27eeb2eda25bff7919e81.patch | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +The raw .patch view of the same Cynosphere dotfiles commit; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/gitdab-cynosphere-dotfiles-command-not-found-commit.md b/research/shell-multiplexer-auto-attach/gitdab-cynosphere-dotfiles-command-not-found-commit.md new file mode 100644 index 0000000..16a5054 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/gitdab-cynosphere-dotfiles-command-not-found-commit.md @@ -0,0 +1,28 @@ +# Cynosphere dotfiles: command_not_found_handler commit + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | zshrc: command not found handler | +| Source URL | https://gitdab.com/Cynosphere/dotfiles-pub/commit/24e04853691cd564f2c27eeb2eda25bff7919e81 | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A personal dotfiles commit defining a Windows-style "not recognized" command_not_found_handler; surfaced by the query, described only briefly by the search tool's own summary, not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/github-pyenv-wiki-unix-shell-initialization.md b/research/shell-multiplexer-auto-attach/github-pyenv-wiki-unix-shell-initialization.md new file mode 100644 index 0000000..47f39ab --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-pyenv-wiki-unix-shell-initialization.md @@ -0,0 +1,47 @@ +# Unix shell initialization (pyenv wiki) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Unix shell initialization | +| Source URL | https://github.com/pyenv/pyenv/wiki/Unix-Shell-initialization | +| Source publisher / author | pyenv project (community wiki) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A third-party wiki page (maintained by the pyenv project, not the fish-shell project) laying out a fuller ordering of fish's startup-file reads: system and user `conf.d` snippet directories, `vendor_conf.d`, then `/etc/fish/config.fish`, and finally `~/.config/fish/config.fish` last — consistent with, and more detailed than, the official fishshell.com docs' own statement that `config.fish` runs after `conf.d` snippets so it can override them. + +## Key findings + +- `~/.config/fish/config.fish` is the *last* file read in fish's startup sequence, meaning anything it does (including tai's plexer-wrap `exec`) runs after every other conf.d snippet and vendor config has already had a chance to run — relevant if another tool's fish integration also lives in conf.d and expects to run before an `exec` replaces the shell process. +- The ordering list itself (system conf.d → user conf.d → vendor_conf.d → `/etc/fish/config.fish` → `~/.config/fish/config.fish`) could not be independently confirmed against the official fishshell.com docs within this search; the search tool's own synthesis explicitly flagged this as "secondary, not confirmed against the official docs." + +## Pros / strengths + +- Goes into more structural detail than the official docs' own high-level statement, which is useful context even though it isn't independently verified here. + +## Cons / limitations / caveats + +- Third-party, community-maintained wiki content, not primary vendor documentation — explicitly treated as secondary per the quality bar in this archive's own README (confidence ratings are honest, not reassuring). The core fact this project actually relies on (config.fish is always sourced, and sourced last) is corroborated by the official docs (fishshell-docs-3-4-index-initialization-files.md) independently of this page. + +## Relevance to tai + +Secondary corroboration, not a load-bearing fact on its own: reinforces that writing the tai hooks into `config.fish` (rather than a `conf.d` snippet) means they run after everything else, which is the right place for an `exec`-based wrap to live (nothing else gets a chance to run after it anyway, by definition). + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-raw-fish-site-current-isatty.md b/research/shell-multiplexer-auto-attach/github-raw-fish-site-current-isatty.md new file mode 100644 index 0000000..620db0a --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-raw-fish-site-current-isatty.md @@ -0,0 +1,28 @@ +# isatty docs, fish-site repo raw "current" mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty - test if a file descriptor is a terminal | +| Source URL | https://raw.githubusercontent.com/fish-shell/fish-site/master/site/docs/current/cmds/isatty.html | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +The raw HTML source of the fish-shell project's own doc-site repo for the "current" isatty page; consistent with the versioned pages already read, not separately opened. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/linux-die-net-fish-1-manpage.md b/research/shell-multiplexer-auto-attach/linux-die-net-fish-1-manpage.md new file mode 100644 index 0000000..860cc81 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/linux-die-net-fish-1-manpage.md @@ -0,0 +1,28 @@ +# fish(1) manpage, linux.die.net mirror (older wording) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fish(1) - Linux man page | +| Source URL | https://linux.die.net/man/1/fish | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An older-wording mirror of the fish(1) manpage; the search tool's own summary flags it (along with the Ubuntu focal copy) as using older phrasing for -i than the current Arch/Debian/openSUSE mirrors. Not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/linux-die-net-isatty-1-posix-utility.md b/research/shell-multiplexer-auto-attach/linux-die-net-isatty-1-posix-utility.md new file mode 100644 index 0000000..35ed33c --- /dev/null +++ b/research/shell-multiplexer-auto-attach/linux-die-net-isatty-1-posix-utility.md @@ -0,0 +1,28 @@ +# isatty(1) Linux man page (unrelated POSIX utility) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty(1) - Linux man page | +| Source URL | https://linux.die.net/man/1/isatty | +| Source type | other | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A different, unrelated isatty(1) command-line utility's man page (not fish's builtin), surfaced as a tangential hit; not independently read and not treated as relevant to fish's own isatty builtin. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/man-archlinux-org-extra-fish-1-en.md b/research/shell-multiplexer-auto-attach/man-archlinux-org-extra-fish-1-en.md new file mode 100644 index 0000000..b7cccc2 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/man-archlinux-org-extra-fish-1-en.md @@ -0,0 +1,28 @@ +# fish(1) manpage, Arch extra-repo mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | man.archlinux.org | +| Source URL | https://man.archlinux.org/man/extra/fish/fish.1.en | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A duplicate Arch Linux manpage mirror (extra-repo path) of the same fish.1 page read in depth at man-archlinux-org-fish-1-en-interactive-flag.md (Tier 1); not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/man-archlinux-org-fish-1-en-interactive-flag.md b/research/shell-multiplexer-auto-attach/man-archlinux-org-fish-1-en-interactive-flag.md new file mode 100644 index 0000000..7b526c0 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/man-archlinux-org-fish-1-en-interactive-flag.md @@ -0,0 +1,48 @@ +# fish(1) manpage: -i/--interactive flag + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | FISH(1) | +| Source URL | https://man.archlinux.org/man/fish.1.en | +| Source publisher / author | fish-shell project, mirrored by the Arch Linux manpage service | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +fish's own command-line options include `-i`/`--interactive`, documented (current wording) as forcing interactive mode regardless of how fish was actually started — the direct fish analogue of bash's own `-i` flag, confirming fish can be spawned the same way (`fish -i`) that the existing bash/zsh integration-test fixtures already spawn `bash -i`/`zsh -i`. + +## Key findings + +- `-i`/`--interactive` sits alongside `-l`/`--login`, `-n`/`--no-execute`, and `-P`/`--private` in fish's own options list — a first-class, stable flag, not a hidden or undocumented one. +- Current wording (Arch, Debian trixie, openSUSE Tumbleweed mirrors) is terse: "the shell is interactive" / forces interactive mode. Older mirrors (Ubuntu 20.04 focal, an older linux.die.net copy) use slightly different directive-style wording ("specify that fish is to run in interactive mode") describing the same behavior — a wording change across fish/doc versions, not a behavior change. +- This directly justifies using `fish -i` to spawn real fish processes in the `FishInstallAndAutoWrapTest`/`FishAntiRecursionTest`/`FishTmuxActiveSkipTest` integration tests in `tests/bashrc.py`, exactly mirroring how the existing bash/zsh tests spawn `bash -i`/`zsh -i`. + +## Pros / strengths + +- Confirmed across multiple independent manpage mirrors (Arch, Debian, openSUSE, plus older Ubuntu/linux.die.net copies) with consistent meaning despite wording differences — high confidence this flag behaves as expected on any fish a real test container would have installed. + +## Cons / limitations / caveats + +- None identified beyond the wording-only discrepancy already noted; doesn't change the behavior being relied on. + +## Relevance to tai + +Directly justified the test design in `tests/bashrc.py`'s `_ShellPlexerHookTestBase._spawn_shell(["-i"], ...)` calls for the fish test classes — proof that `fish -i` is a real, stable, documented way to force a real interactive fish process for the end-to-end auto-wrap/anti-recursion/tmux-skip tests, run for real against the fish binary installed in `tests/docker/Dockerfile`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/man-archlinux-org-fish-index.md b/research/shell-multiplexer-auto-attach/man-archlinux-org-fish-index.md new file mode 100644 index 0000000..94c258f --- /dev/null +++ b/research/shell-multiplexer-auto-attach/man-archlinux-org-fish-index.md @@ -0,0 +1,28 @@ +# fish manpage index, Arch mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | FISH(1) | fish-shell | FISH(1) | | +| Source URL | https://man.archlinux.org/man/fish | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An index-style listing page on the same Arch manpage mirror as the Tier 1 fish.1 page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/mynixos-home-manager-programs-fish-functions.md b/research/shell-multiplexer-auto-attach/mynixos-home-manager-programs-fish-functions.md new file mode 100644 index 0000000..c43a3ad --- /dev/null +++ b/research/shell-multiplexer-auto-attach/mynixos-home-manager-programs-fish-functions.md @@ -0,0 +1,28 @@ +# Nix home-manager programs.fish.functions option page + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | mynixos.com | +| Source URL | https://mynixos.com/home-manager/option/programs.fish.functions | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A Nix home-manager option reference page for declaring fish functions (which is how fish_command_not_found would be set via Nix); tangential, not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/neowin-fish-4-2-release.md b/research/shell-multiplexer-auto-attach/neowin-fish-4-2-release.md new file mode 100644 index 0000000..9322ba7 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/neowin-fish-4-2-release.md @@ -0,0 +1,28 @@ +# Neowin article on fish 4.2 release + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Fish 4.2 released with several bug fixes and improvements | +| Source URL | https://www.neowin.net/amp/fish-42-released-with-several-bug-fixes-and-improvements/ | +| Source type | blog post | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A news article covering the fish 4.2 release, surfaced while trying to date isatty's introduction; not independently read, and fish 4.2 postdates isatty's documented presence in 3.2 regardless. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/openmamba-src-fish-3-4-0-1mamba.md b/research/shell-multiplexer-auto-attach/openmamba-src-fish-3-4-0-1mamba.md new file mode 100644 index 0000000..be43478 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/openmamba-src-fish-3-4-0-1mamba.md @@ -0,0 +1,28 @@ +# openmamba fish 3.4.0-1mamba source page + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | A user friendly command line shell for UNIX-like operating systems such as Linux https://fishshell.com | +| Source URL | https://src.openmamba.org/rpms/fish/src/tag/3.4.0-1mamba | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An openmamba distro packaging source-tag page for fish 3.4.0, surfaced while trying to date isatty's introduction; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/openmamba-src-fish-3-4-1-1mamba.md b/research/shell-multiplexer-auto-attach/openmamba-src-fish-3-4-1-1mamba.md new file mode 100644 index 0000000..164556f --- /dev/null +++ b/research/shell-multiplexer-auto-attach/openmamba-src-fish-3-4-1-1mamba.md @@ -0,0 +1,28 @@ +# openmamba fish 3.4.1-1mamba source page + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | A user friendly command line shell for UNIX-like operating systems such as Linux https://fishshell.com | +| Source URL | https://src.openmamba.org/rpms/fish/src/tag/3.4.1-1mamba | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another openmamba distro packaging source-tag page, for fish 3.4.1; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/opensuse-manpages-leap15-6-cnf-command-not-found.md b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap15-6-cnf-command-not-found.md new file mode 100644 index 0000000..48ab02a --- /dev/null +++ b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap15-6-cnf-command-not-found.md @@ -0,0 +1,28 @@ +# openSUSE Leap 15.6 cnf(1) manpage + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Scroll to navigation | +| Source URL | https://manpages.opensuse.org/Leap-15.6/scout-command-not-found/cnf.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +The manpage for openSUSE's own command-not-found helper (cnf), surfaced by the query as a related but distinct mechanism; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/opensuse-manpages-leap15-6-fish-1.md b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap15-6-fish-1.md new file mode 100644 index 0000000..b0dbbb5 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap15-6-fish-1.md @@ -0,0 +1,28 @@ +# fish(1) manpage, openSUSE Leap 15.6 mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://manpages.opensuse.org/Leap-15.6/fish/fish.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An openSUSE Leap 15.6 manpage-archive mirror of the fish(1) reference; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/opensuse-manpages-leap16-0-fish-1.md b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap16-0-fish-1.md new file mode 100644 index 0000000..3c47fe5 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap16-0-fish-1.md @@ -0,0 +1,28 @@ +# fish(1) manpage, openSUSE Leap 16.0 mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://manpages.opensuse.org/Leap-16.0/fish/fish.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An openSUSE Leap 16.0 manpage-archive mirror of the fish(1) reference; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/opensuse-manpages-leap16-0-fish-doc-1.md b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap16-0-fish-doc-1.md new file mode 100644 index 0000000..bca8cb3 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/opensuse-manpages-leap16-0-fish-doc-1.md @@ -0,0 +1,28 @@ +# fish-doc(1) manpage, openSUSE Leap 16.0 mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://manpages.opensuse.org/Leap-16.0/fish/fish-doc.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another openSUSE manpage-archive mirror (Leap 16.0) of the same fish-doc(1) reference; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/opensuse-manpages-tumbleweed-fish-1.md b/research/shell-multiplexer-auto-attach/opensuse-manpages-tumbleweed-fish-1.md new file mode 100644 index 0000000..8757756 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/opensuse-manpages-tumbleweed-fish-1.md @@ -0,0 +1,28 @@ +# fish(1) manpage, openSUSE Tumbleweed mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://manpages.opensuse.org/Tumbleweed/fish/fish.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An openSUSE manpage-archive mirror of the fish(1) reference, matching the current wording confirmed on the Arch mirror; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/opensuse-manpages-tumbleweed-fish-doc-1.md b/research/shell-multiplexer-auto-attach/opensuse-manpages-tumbleweed-fish-doc-1.md new file mode 100644 index 0000000..5929025 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/opensuse-manpages-tumbleweed-fish-doc-1.md @@ -0,0 +1,28 @@ +# fish-doc(1) manpage, openSUSE Tumbleweed mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | table of contents | +| Source URL | https://manpages.opensuse.org/Tumbleweed/fish/fish-doc.1.en.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An openSUSE manpage-archive mirror of the fish-doc(1) reference; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/oracle-docs-e62101-zshbuiltins.md b/research/shell-multiplexer-auto-attach/oracle-docs-e62101-zshbuiltins.md new file mode 100644 index 0000000..0800665 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/oracle-docs-e62101-zshbuiltins.md @@ -0,0 +1,28 @@ +# Oracle zshbuiltins manual mirror (E62101_01) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Go to main content | +| Source URL | https://docs.oracle.com/cd/E62101_01/html/E62874/zshbuiltins-1.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Another duplicate Oracle Solaris documentation mirror of the same zshbuiltins manual page; not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/oracle-docs-e88353-zshbuiltins-autoload-x.md b/research/shell-multiplexer-auto-attach/oracle-docs-e88353-zshbuiltins-autoload-x.md new file mode 100644 index 0000000..c3e2904 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/oracle-docs-e88353-zshbuiltins-autoload-x.md @@ -0,0 +1,47 @@ +# zshbuiltins(1): autoload -X (Oracle Solaris docs mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | zshbuiltins(1) | +| Source URL | https://docs.oracle.com/cd/E88353_01/html/E37839/zshbuiltins-1.html | +| Source publisher / author | Oracle (Solaris documentation, mirroring the upstream zsh manual) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +The formal zsh builtins manual definition of `autoload -X`: marks the *calling function itself* for autoloading, then immediately loads and executes it with the current positional parameters, failing with an error (and leaving the function undefined but marked for autoload) if no definition is found on `$fpath`. + +## Key findings + +- `autoload -X` is specifically for the case where a function calls itself recursively to trigger its own on-demand loading — exactly the trick the 2015 `command_not_found_handler` example (Tier 1, zsh-org-mla-2015-msg00931) uses. +- This is a general zsh function-loading mechanism, not something specific to `command_not_found_handler` — it happens to be a convenient tool for *implementing* a dynamic handler, not a required part of defining one. + +## Pros / strengths + +- The authoritative formal definition, which let this research confirm precisely what the 2015 mailing-list example's `autoload +X` call was actually doing mechanically, rather than taking the example purely on faith. + +## Cons / limitations / caveats + +- Mirrored vendor documentation (Oracle's Solaris docs), not the canonical zsh.org manual itself; the content matches the well-known standard zsh behavior, so this is treated as a faithful, non-divergent mirror. + +## Relevance to tai + +Confirmed that `autoload -X` is orthogonal to tai's own needs: `ZSH_NOTFOUND_BLOCK` defines a plain, statically-written `command_not_found_handler` with no dynamic loading, so this mechanism is not used in tai.py at all — this dossier's main value was ruling it out as something tai's own handler needed to account for. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/oracle-docs-f16636-zshbuiltins.md b/research/shell-multiplexer-auto-attach/oracle-docs-f16636-zshbuiltins.md new file mode 100644 index 0000000..bcb3a61 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/oracle-docs-f16636-zshbuiltins.md @@ -0,0 +1,28 @@ +# Oracle zshbuiltins manual mirror (F16636_01) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Go to main content | +| Source URL | https://docs.oracle.com/cd/F16636_01/html/E71066/zshbuiltins-1.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A duplicate Oracle Solaris documentation mirror of the zshbuiltins manual page (autoload -X), same content as the E88353_01 mirror read in depth (Tier 1); not separately read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/pkg-go-dev-notfoundle.md b/research/shell-multiplexer-auto-attach/pkg-go-dev-notfoundle.md new file mode 100644 index 0000000..f279397 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/pkg-go-dev-notfoundle.md @@ -0,0 +1,28 @@ +# notfoundle Go package (pkg.go.dev) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | pkg.go.dev | +| Source URL | https://pkg.go.dev/github.com/brackendawson/notfoundle | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A tangential hit from the fish_command_not_found query -- a Go library with a similar name, unrelated to fish's own mechanism; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/rocketleaguechatp-mirror-fish-command-not-found-manpage.md b/research/shell-multiplexer-auto-attach/rocketleaguechatp-mirror-fish-command-not-found-manpage.md new file mode 100644 index 0000000..74da5bd --- /dev/null +++ b/research/shell-multiplexer-auto-attach/rocketleaguechatp-mirror-fish-command-not-found-manpage.md @@ -0,0 +1,28 @@ +# fish_command_not_found manpage (third-party tarball mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fish command not found.1 | +| Source URL | https://rocketleaguechatp.duckdns.org/tarball_dev-kde/usr/share/fish/man/man1/fish_command_not_found.1 | +| Source type | other | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An unofficial third-party mirror of the fish manpage tarball turned up by the search; not independently read, and not treated as authoritative given the official fishshell.com docs cover the same content. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/rocketleaguechatp-mirror-isatty-manpage.md b/research/shell-multiplexer-auto-attach/rocketleaguechatp-mirror-isatty-manpage.md new file mode 100644 index 0000000..522ba5b --- /dev/null +++ b/research/shell-multiplexer-auto-attach/rocketleaguechatp-mirror-isatty-manpage.md @@ -0,0 +1,28 @@ +# isatty manpage (third-party tarball mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | rocketleaguechatp.duckdns.org | +| Source URL | https://rocketleaguechatp.duckdns.org/tarball_dev/usr/share/fish/man/man1/isatty.1 | +| Source type | other | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An unofficial third-party mirror of the fish isatty manpage tarball; not independently read, official fishshell.com docs preferred. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/ubuntu-manpages-fish-1-focal.md b/research/shell-multiplexer-auto-attach/ubuntu-manpages-fish-1-focal.md new file mode 100644 index 0000000..f5fae54 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/ubuntu-manpages-fish-1-focal.md @@ -0,0 +1,28 @@ +# fish(1) manpage, Ubuntu focal mirror (older wording) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Ubuntu Manpages | +| Source URL | https://manpages.ubuntu.com/manpages/focal/man1/fish.1.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An older Ubuntu (20.04 focal) mirror of the fish(1) manpage, using the older directive-style wording for -i per the search tool's own comparison; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/ubuntu-manpages-fish-doc-1-resolute.md b/research/shell-multiplexer-auto-attach/ubuntu-manpages-fish-doc-1-resolute.md new file mode 100644 index 0000000..5502c48 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/ubuntu-manpages-fish-doc-1-resolute.md @@ -0,0 +1,28 @@ +# fish-doc(1) manpage, Ubuntu mirror (resolute) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Ubuntu Manpages | +| Source URL | https://manpages.ubuntu.com/manpages/resolute/man1/fish-doc.1.html | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +An Ubuntu manpage-archive mirror of the fish-doc(1) reference; not independently read, official fishshell.com docs preferred. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/w3cub-fish-4-5-fish-command-not-found.md b/research/shell-multiplexer-auto-attach/w3cub-fish-4-5-fish-command-not-found.md new file mode 100644 index 0000000..053744c --- /dev/null +++ b/research/shell-multiplexer-auto-attach/w3cub-fish-4-5-fish-command-not-found.md @@ -0,0 +1,28 @@ +# fish_command_not_found docs mirror (w3cub, fish 4.5) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | fish command not found | +| Source URL | https://docs.w3cub.com/fish~4.5/cmds/fish_command_not_found | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A third-party doc-mirror site's copy of the fish 4.5 fish_command_not_found page; not independently read, official fishshell.com docs preferred. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/w3cub-fish-4-5-isatty.md b/research/shell-multiplexer-auto-attach/w3cub-fish-4-5-isatty.md new file mode 100644 index 0000000..17e2167 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/w3cub-fish-4-5-isatty.md @@ -0,0 +1,28 @@ +# isatty builtin docs mirror (w3cub, fish 4.5) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | isatty | +| Source URL | https://docs.w3cub.com/fish~4.5/cmds/isatty | +| Source type | vendor docs | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A third-party doc-mirror site's copy of the fish 4.5 isatty page; not independently read, official fishshell.com docs preferred. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/webartifex-oh-my-zsh-command-not-found-refactor.md b/research/shell-multiplexer-auto-attach/webartifex-oh-my-zsh-command-not-found-refactor.md new file mode 100644 index 0000000..61c3ac8 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/webartifex-oh-my-zsh-command-not-found-refactor.md @@ -0,0 +1,28 @@ +# oh-my-zsh command-not-found plugin refactor commit + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | refactor(command-not-found): clean up and reorganize logic | +| Source URL | https://code.webartifex.biz/alexander/oh-my-zsh/commit/b54a6dab9cc9db9196aae5d8bc53ac3956855ecf | +| Source type | repository | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A commit refactoring oh-my-zsh's command-not-found plugin (which calls Debian's command-not-found tool from inside command_not_found_handler, falling back to a printf + exit 127); described briefly by the search tool's summary, not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/wiki-archlinux-org-fish-page.md b/research/shell-multiplexer-auto-attach/wiki-archlinux-org-fish-page.md new file mode 100644 index 0000000..b1d0229 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/wiki-archlinux-org-fish-page.md @@ -0,0 +1,28 @@ +# Arch Wiki: Fish + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | wiki.archlinux.org | +| Source URL | https://wiki.archlinux.org/title/Fish | +| Source type | forum thread | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +The Arch Linux community wiki's fish page (also reachable via the older /index.php/Fish URL form), surfaced while researching config.fish sourcing behavior; not independently read, official fishshell.com docs preferred as the authoritative source. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/zenn-dev-estra-fish-typeof-commands.md b/research/shell-multiplexer-auto-attach/zenn-dev-estra-fish-typeof-commands.md new file mode 100644 index 0000000..b516636 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/zenn-dev-estra-fish-typeof-commands.md @@ -0,0 +1,28 @@ +# zenn.dev article on fish command introspection + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | zenn fish typeof commands | +| Source URL | https://zenn.dev/estra/articles/zenn-fish-typeof-commands | +| Source type | blog post | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A Japanese-language blog post surfaced by the fish_command_not_found query, tangentially related to fish command introspection; not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/zsh-org-mla-2009-msg00989-command-not-found-handler.md b/research/shell-multiplexer-auto-attach/zsh-org-mla-2009-msg00989-command-not-found-handler.md new file mode 100644 index 0000000..1b85301 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/zsh-org-mla-2009-msg00989-command-not-found-handler.md @@ -0,0 +1,47 @@ +# zsh-users mailing list: minimal command_not_found_handler example (Peter Stephenson, 2009) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Zsh Mailing List Archive (zsh-users, 2009, msg00989) | +| Source URL | https://zsh.org/mla/users/2009/msg00989.html | +| Source publisher / author | Peter Stephenson, via the zsh-users mailing list (zsh.org) | +| Source type | forum thread | +| Source date (published) | 2009 (exact date not stated in the search result) | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A much simpler, minimal `command_not_found_handler` example from a second zsh core contributor: checks whether the missing command equals a specific name (`foo`), prints a message and returns 0 if so, and otherwise returns 127 — the return code that reproduces zsh's normal "command not found" message and exit status for every other missing command. + +## Key findings + +- Returning 127 from the handler for anything it doesn't want to specially handle is the documented way to fall through to zsh's normal "command not found" behavior and exit status, rather than silently swallowing the error. +- This example is structurally the simplest possible `command_not_found_handler`: no autoload, no dynamic loading, just a plain conditional and a return code — directly comparable in shape to what tai.py actually needed. + +## Pros / strengths + +- A minimal, directly comparable example (vs. the more advanced 2015 autoload-based one) that maps cleanly onto tai's own simple "forward everything to the tai binary" use case. + +## Cons / limitations / caveats + +- tai's own handler doesn't need the 127-fallthrough behavior this example demonstrates, since it unconditionally forwards every missing command to the tai binary rather than special-casing one and falling through for the rest — noted here as a deliberate difference, not an oversight. + +## Relevance to tai + +Reinforces (independently of the 2015 example) that `command_not_found_handler` is just a plain zsh function, confirming there's no hidden registration step `ZSH_NOTFOUND_BLOCK` would need to account for beyond the function definition itself. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/zsh-org-mla-2015-msg00929-reply.md b/research/shell-multiplexer-auto-attach/zsh-org-mla-2015-msg00929-reply.md new file mode 100644 index 0000000..ee27332 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/zsh-org-mla-2015-msg00929-reply.md @@ -0,0 +1,28 @@ +# zsh-users mailing list, 2015 thread reply + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Zsh Mailing List Archive | +| Source URL | https://zsh.org/mla/users/2015/msg00929.html | +| Source type | forum thread | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +A reply in the same 2015 zsh-users thread as the Bart Schaefer example (Tier 1); surfaced by the query but not independently read in depth. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/zsh-org-mla-2015-msg00931-command-not-found-handler.md b/research/shell-multiplexer-auto-attach/zsh-org-mla-2015-msg00931-command-not-found-handler.md new file mode 100644 index 0000000..ad08190 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/zsh-org-mla-2015-msg00931-command-not-found-handler.md @@ -0,0 +1,48 @@ +# zsh-users mailing list: command_not_found_handler autoload example (Bart Schaefer, 2015) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Zsh Mailing List Archive (zsh-users, 2015, msg00931) | +| Source URL | https://www.zsh.org/mla/users/2015/msg00931.html | +| Source publisher / author | Bart Schaefer, via the zsh-users mailing list (zsh.org) | +| Source type | forum thread | +| Source date (published) | 2015 (exact date not stated in the search result) | +| Date researched | 2026-10-10 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A zsh core contributor's own worked example of `command_not_found_handler`: a zsh-recognized function name that fires when zsh's normal `$fpath`/`$path` lookup fails to find a command, with the example showing it dynamically registering (`FPATH=$PWD; autoload +X $missing_command`) and running a function on the fly before falling through to the usual error. + +## Key findings + +- `command_not_found_handler` is a plain zsh function definition, recognized by name — the zsh analogue of bash's `command_not_found_handle`, differing only in the `handler`/`handle` suffix. No autoload directive is required just to make zsh call it; autoload is only used inside the example body for its own dynamic-function-loading trick, which is unrelated to the hook mechanism itself being active. +- The handler runs *after* the parent shell has already forked for the failed command, per Bart Schaefer's own note in the thread — so anything the handler loads or defines does not persist back into the parent shell's own function table. +- The example demonstrates `autoload -X` being used *inside* a handler for a specific advanced use case (lazy-loading an unknown function from the current directory); this is a feature of the example's specific technique, not a requirement for using `command_not_found_handler` at all. + +## Pros / strengths + +- Directly from a zsh core contributor, carrying real authority on the mechanism's actual semantics (the fork-before-handler timing detail in particular is not the kind of thing casual documentation usually states explicitly). + +## Cons / limitations / caveats + +- The worked example itself is solving a different, more advanced problem (dynamic function loading) than tai's simple delegate-to-a-binary use case; only the top-level fact (the handler's name and that it's a plain function) was load-bearing for this project, not the autoload trick inside it. + +## Relevance to tai + +Confirms the exact function name (`command_not_found_handler`, not `_handle`) that `ZSH_NOTFOUND_BLOCK` in `tai.py` defines, and confirms no special autoload/registration step is needed beyond the function definition itself — consistent with the simple, bash-parallel block tai.py actually writes. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-10 | Initial dossier created. | diff --git a/tai.py b/tai.py index 59d72e5..39ddc22 100755 --- a/tai.py +++ b/tai.py @@ -178,6 +178,23 @@ BASHRC_BLOCK = ( " return $?\n" "}\n" + BASHRC_MARK_END + "\n" ) +ZSH_NOTFOUND_BLOCK = ( + BASHRC_MARK_BEGIN + "\n" + "alias tai=\"$HOME/.local/bin/tai.py\"\n" + "command_not_found_handler() {\n" + " \"$HOME/.local/bin/tai.py\" \"$@\"\n" + " return $?\n" + "}\n" + BASHRC_MARK_END + "\n" +) +FISH_NOTFOUND_BLOCK = ( + BASHRC_MARK_BEGIN + "\n" + "function tai\n" + " \"$HOME/.local/bin/tai.py\" $argv\n" + "end\n" + "function fish_command_not_found\n" + " \"$HOME/.local/bin/tai.py\" $argv\n" + "end\n" + BASHRC_MARK_END + "\n" +) BOX_CONTAINERFILE = """FROM python:3.12-slim ENV DEBIAN_FRONTEND=noninteractive PIP_NO_CACHE_DIR=1 RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg espeak-ng python3-venv && rm -rf /var/lib/apt/lists/* @@ -240,7 +257,7 @@ RestartSec=10 [Install] WantedBy=default.target """ -INSTALL_TARGETS = ("binary", "bash-hook", "plexer-hook", "venv", "scheduler-service", "telegram-service", "container") +INSTALL_TARGETS = ("binary", "bash-hook", "zsh-hook", "fish-hook", "plexer-hook", "zsh-plexer-hook", "fish-plexer-hook", "venv", "scheduler-service", "telegram-service", "container") CONTEXT_CAP = 32000 COMPACT_RATIO = 0.8 KEEP_TURNS = 6 @@ -4619,7 +4636,7 @@ TOOL_SCHEMAS = [ tool_schema("restore", "Restore a file to an audit row: the post-image for writes and edits, the pre-image for deletes and shell snapshots. Refuses truncated images. Asks the user first.", {"id": {"type": "integer"}}, ["id"]), tool_schema("release", "Cut a release of the running script: bump VERSION by part (patch for fixes, minor for features, major for breaking changes), keep a versioned backup, log the message in the audit trail. Asks the user first.", {"part": {"type": "string"}, "message": {"type": "string"}}, ["part", "message"]), tool_schema("tags", "List vault tags with usage counts, most used first. Consult before tagging so new items reuse established tags instead of inventing synonyms.", {"prefix": {"type": "string"}, "limit": {"type": "integer"}}, []), - tool_schema("install", "Manage tai installations: binary, bash command-not-found hook, venv, scheduler service, telegram service, container. Status reports what exists. Install adds missing pieces, upgrade refreshes in place and restarts services, reinstall rebuilds artifacts, uninstall removes them. Service changes take effect immediately. The vault is never touched by any action. Non-status actions ask the user first.", {"action": {"type": "string"}, "targets": {"type": "array"}}, ["action"]), + tool_schema("install", "Manage tai installations: binary, command-not-found hook and plexer auto-wrap hook for bash/zsh/fish, venv, scheduler service, telegram service, container. Status reports what exists. Install adds missing pieces (skipping a shell hook loudly if that shell isn't on the machine), upgrade refreshes in place and restarts services, reinstall rebuilds artifacts, uninstall removes them. Service changes take effect immediately. The vault is never touched by any action. Non-status actions ask the user first.", {"action": {"type": "string"}, "targets": {"type": "array"}}, ["action"]), tool_schema("create_bot", "Create a bot in the current profile: a name plus its own system message and history. Give at least one of description, rules, or behavior; optional nicknames register as @mention aliases (a short form of the name registers automatically). Names are lowercase.", {"name": {"type": "string"}, "description": {"type": "string"}, "rules": {"type": "string"}, "behavior": {"type": "string"}, "nicknames": {"type": "array"}}, ["name"]), tool_schema("search", "Search everything at once with ranked full-text matching (SQLite FTS5 + bm25, no vectors): records, episodic events, the audit trail, and RAG chunks. Chunks are the retrieval-grade granular hits (e.g. one slice of an exported terminal log) and come back with their actual text, not just a snippet; use the chunk's parent id with record_read to see more surrounding context. One call replaces paging through each store separately. expand adds one graph hop per record hit.", {"query": {"type": "string"}, "kinds": {"type": "array"}, "limit": {"type": "integer"}, "expand": {"type": "boolean"}}, ["query"]), ] @@ -4684,7 +4701,7 @@ TOOL_TAGS = { "restore": ("restore", "undo", "revert", "recover", "rollback", "undelete", "previous"), "release": ("release", "version", "bump", "changelog", "publish"), "tags": ("tags", "tag", "label", "categorize", "taxonomy", "vocabulary"), - "install": ("install", "uninstall", "reinstall", "upgrade", "setup", "service", "systemd", "venv", "hook", "deploy"), + "install": ("install", "uninstall", "reinstall", "upgrade", "setup", "service", "systemd", "venv", "hook", "deploy", "zsh", "fish"), "create_bot": ("bot", "bots", "persona", "character", "mention"), "search": ("search", "find", "lookup", "everything"), } @@ -5378,6 +5395,18 @@ PLEXER_BASHRC_BLOCK = ( "fi\n" + PLEXER_BASHRC_MARK_END + "\n" ) +FISH_PLEXER_BLOCK = ( + PLEXER_BASHRC_MARK_BEGIN + "\n" + "if status is-interactive\n" + "and test -z \"$TAI_PLEXER_ACTIVE\"\n" + "and test -z \"$TMUX\"\n" + "and not test -e \"$HOME/.tai-no-plexer\"\n" + "and isatty stdin\n" + "and isatty stdout\n" + " exec \"$HOME/.local/bin/tai.py\" --plexer-wrap\n" + "end\n" + + PLEXER_BASHRC_MARK_END + "\n" +) class Tools: @@ -7851,21 +7880,40 @@ def script_version(path): return ".".join(match.groups()) if match else None -def hook_present(path): +def shell_rc_path(shell): + """Where each shell's startup file lives. bash/zsh: a dotfile sourced + for interactive shells only. fish: config.fish, which fish sources on + every startup (interactive or not, login or not) -- hence the + `status is-interactive` guard inside FISH_PLEXER_BLOCK rather than + relying on the file only being read at the right time.""" + home = os.path.expanduser("~") + if shell == "fish": + config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(home, ".config") + return os.path.join(config_home, "fish", "config.fish") + if shell == "zsh": + return os.path.join(home, ".zshrc") + return os.path.join(home, ".bashrc") + + +def shell_present(shell): + return shutil.which(shell) is not None + + +def _rc_block_present(path, block): try: with open(path, "r", encoding="utf-8") as handle: - return BASHRC_BLOCK in handle.read() + return block in handle.read() except OSError: return False -def remove_bashrc_block(path): +def _rc_block_remove(path, mark_begin, mark_end): try: with open(path, "r", encoding="utf-8") as handle: raw = handle.read() except OSError: return False - stripped = re.sub(re.escape(BASHRC_MARK_BEGIN) + r".*?" + re.escape(BASHRC_MARK_END) + r"\n?", "", raw, flags=re.DOTALL) + stripped = re.sub(re.escape(mark_begin) + r".*?" + re.escape(mark_end) + r"\n?", "", raw, flags=re.DOTALL) if stripped == raw: return False backup = path + ".bak-tai" @@ -7877,52 +7925,101 @@ def remove_bashrc_block(path): return True -def plexer_hook_present(path): - try: - with open(path, "r", encoding="utf-8") as handle: - return PLEXER_BASHRC_BLOCK in handle.read() - except OSError: - return False - - -def remove_plexer_bashrc_block(path): - try: - with open(path, "r", encoding="utf-8") as handle: - raw = handle.read() - except OSError: - return False - stripped = re.sub(re.escape(PLEXER_BASHRC_MARK_BEGIN) + r".*?" + re.escape(PLEXER_BASHRC_MARK_END) + r"\n?", "", raw, flags=re.DOTALL) - if stripped == raw: - return False - backup = path + ".bak-tai" - if raw and not os.path.exists(backup): - with open(backup, "w", encoding="utf-8") as handle: - handle.write(raw) - with open(path, "w", encoding="utf-8") as handle: - handle.write(stripped) - return True - - -def upsert_plexer_bashrc_block(path): +def _rc_block_upsert(path, mark_begin, mark_end, block): try: with open(path, "r", encoding="utf-8") as handle: raw = handle.read() except OSError: raw = "" - if PLEXER_BASHRC_BLOCK in raw: + if block in raw: return False backup = path + ".bak-tai" if raw and not os.path.exists(backup): with open(backup, "w", encoding="utf-8") as handle: handle.write(raw) - stripped = re.sub(re.escape(PLEXER_BASHRC_MARK_BEGIN) + r".*?" + re.escape(PLEXER_BASHRC_MARK_END) + r"\n?", "", raw, flags=re.DOTALL) + stripped = re.sub(re.escape(mark_begin) + r".*?" + re.escape(mark_end) + r"\n?", "", raw, flags=re.DOTALL) if stripped and not stripped.endswith("\n"): stripped += "\n" + parent = os.path.dirname(path) + if parent and not os.path.isdir(parent): + os.makedirs(parent, exist_ok=True) with open(path, "w", encoding="utf-8") as handle: - handle.write(stripped + PLEXER_BASHRC_BLOCK) + handle.write(stripped + block) return True +def hook_present(path): + return _rc_block_present(path, BASHRC_BLOCK) + + +def remove_bashrc_block(path): + return _rc_block_remove(path, BASHRC_MARK_BEGIN, BASHRC_MARK_END) + + +def zsh_hook_present(path): + return _rc_block_present(path, ZSH_NOTFOUND_BLOCK) + + +def remove_zsh_hook_block(path): + return _rc_block_remove(path, BASHRC_MARK_BEGIN, BASHRC_MARK_END) + + +def upsert_zsh_hook_block(path): + return _rc_block_upsert(path, BASHRC_MARK_BEGIN, BASHRC_MARK_END, ZSH_NOTFOUND_BLOCK) + + +def fish_hook_present(path): + return _rc_block_present(path, FISH_NOTFOUND_BLOCK) + + +def remove_fish_hook_block(path): + return _rc_block_remove(path, BASHRC_MARK_BEGIN, BASHRC_MARK_END) + + +def upsert_fish_hook_block(path): + return _rc_block_upsert(path, BASHRC_MARK_BEGIN, BASHRC_MARK_END, FISH_NOTFOUND_BLOCK) + + +def plexer_hook_present(path): + return _rc_block_present(path, PLEXER_BASHRC_BLOCK) + + +def remove_plexer_bashrc_block(path): + return _rc_block_remove(path, PLEXER_BASHRC_MARK_BEGIN, PLEXER_BASHRC_MARK_END) + + +def upsert_plexer_bashrc_block(path): + return _rc_block_upsert(path, PLEXER_BASHRC_MARK_BEGIN, PLEXER_BASHRC_MARK_END, PLEXER_BASHRC_BLOCK) + + +# zsh reuses PLEXER_BASHRC_BLOCK byte-for-byte -- zsh is POSIX-enough that +# the plain `[`/`case "$-"` bash content works unchanged; only the target +# file differs (~/.zshrc, not ~/.bashrc). No ZSH_PLEXER_BLOCK constant exists +# on purpose -- there is nothing shell-specific to put in one. +def zsh_plexer_hook_present(path): + return _rc_block_present(path, PLEXER_BASHRC_BLOCK) + + +def remove_zsh_plexer_block(path): + return _rc_block_remove(path, PLEXER_BASHRC_MARK_BEGIN, PLEXER_BASHRC_MARK_END) + + +def upsert_zsh_plexer_block(path): + return _rc_block_upsert(path, PLEXER_BASHRC_MARK_BEGIN, PLEXER_BASHRC_MARK_END, PLEXER_BASHRC_BLOCK) + + +def fish_plexer_hook_present(path): + return _rc_block_present(path, FISH_PLEXER_BLOCK) + + +def remove_fish_plexer_block(path): + return _rc_block_remove(path, PLEXER_BASHRC_MARK_BEGIN, PLEXER_BASHRC_MARK_END) + + +def upsert_fish_plexer_block(path): + return _rc_block_upsert(path, PLEXER_BASHRC_MARK_BEGIN, PLEXER_BASHRC_MARK_END, FISH_PLEXER_BLOCK) + + def box_venv_state(engine): try: probe = box_exec(engine, ["test", "-x", BOX_PYTHON], timeout=30) @@ -7931,6 +8028,12 @@ def box_venv_state(engine): return "venv ready" if probe.returncode == 0 else "no venv (reinstall container to rebuild)" +def shell_hook_status_line(label, shell, present_fn): + path = shell_rc_path(shell) + note = "" if shell_present(shell) else " (%s not found)" % shell + return "%s: %s in %s%s" % (label, "installed" if present_fn(path) else "not installed", path, note) + + def status_lines(home, store=None): lines = [] binary = os.path.join(os.path.expanduser("~"), ".local", "bin", "tai.py") @@ -7939,7 +8042,11 @@ def status_lines(home, store=None): else: lines.append("binary: not installed") lines.append("bash-hook: %s in ~/.bashrc" % ("installed" if hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed")) + lines.append(shell_hook_status_line("zsh-hook", "zsh", zsh_hook_present)) + lines.append(shell_hook_status_line("fish-hook", "fish", fish_hook_present)) lines.append("plexer-hook: %s in ~/.bashrc" % ("installed" if plexer_hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed")) + lines.append(shell_hook_status_line("zsh-plexer-hook", "zsh", zsh_plexer_hook_present)) + lines.append(shell_hook_status_line("fish-plexer-hook", "fish", fish_plexer_hook_present)) python = venv_python(home) if python: lines.append("venv: ready %s" % python) @@ -8040,6 +8147,50 @@ def op_plexer_hook(mode, home): return "plexer-hook: already present" +def op_shell_hook(mode, shell, label, upsert_fn, remove_fn, source_hint): + """Shared body for the zsh/fish hook variants -- mirrors op_hook/op_plexer_hook + exactly, plus one addition: install/upgrade skip loudly (never write a dangling + hook into a config file for a shell that isn't even on this machine) when the + shell binary isn't found. uninstall/reinstall/status still act on the file + regardless, so a hook installed before a shell was later removed still cleans up.""" + path = shell_rc_path(shell) + if mode in ("install", "upgrade") and not shell_present(shell): + return "%s: %s not found, skipped" % (label, shell) + if mode == "uninstall": + return "%s: %s" % (label, "removed" if remove_fn(path) else "not present") + if mode == "reinstall": + remove_fn(path) + try: + changed = upsert_fn(path) + except OSError as exc: + return "%s: write failed: %s" % (label, short_error(exc)) + if changed: + return "%s: installed, run: %s" % (label, source_hint) + return "%s: already present" % label + + +def op_zsh_hook(mode, home): + return op_shell_hook(mode, "zsh", "zsh-hook", upsert_zsh_hook_block, remove_zsh_hook_block, "source ~/.zshrc") + + +def op_fish_hook(mode, home): + return op_shell_hook(mode, "fish", "fish-hook", upsert_fish_hook_block, remove_fish_hook_block, "open a new fish shell") + + +def op_zsh_plexer_hook(mode, home): + return op_shell_hook( + mode, "zsh", "zsh-plexer-hook", upsert_zsh_plexer_block, remove_zsh_plexer_block, + "source ~/.zshrc (new interactive shells auto-wrap; existing ones are unaffected until restarted)", + ) + + +def op_fish_plexer_hook(mode, home): + return op_shell_hook( + mode, "fish", "fish-plexer-hook", upsert_fish_plexer_block, remove_fish_plexer_block, + "open a new fish shell (new interactive shells auto-wrap; existing ones are unaffected until restarted)", + ) + + def op_venv(mode, home): if mode == "uninstall": folder = venv_dir(home) @@ -8152,8 +8303,16 @@ def install_report(action, targets, home, store=None): lines.append(op_binary(action, home)) elif target == "bash-hook": lines.append(op_hook(action, home)) + elif target == "zsh-hook": + lines.append(op_zsh_hook(action, home)) + elif target == "fish-hook": + lines.append(op_fish_hook(action, home)) elif target == "plexer-hook": lines.append(op_plexer_hook(action, home)) + elif target == "zsh-plexer-hook": + lines.append(op_zsh_plexer_hook(action, home)) + elif target == "fish-plexer-hook": + lines.append(op_fish_plexer_hook(action, home)) elif target == "venv": lines.append(op_venv(action, home)) elif target == "scheduler-service": @@ -8236,31 +8395,27 @@ def uninstall_telegram(): def upsert_bashrc_block(path): - try: - with open(path, "r", encoding="utf-8") as handle: - raw = handle.read() - except OSError: - raw = "" - if BASHRC_BLOCK in raw: - return False - backup = path + ".bak-tai" - if raw and not os.path.exists(backup): - with open(backup, "w", encoding="utf-8") as handle: - handle.write(raw) - stripped = re.sub(re.escape(BASHRC_MARK_BEGIN) + r".*?" + re.escape(BASHRC_MARK_END) + r"\n?", "", raw, flags=re.DOTALL) - if stripped and not stripped.endswith("\n"): - stripped += "\n" - with open(path, "w", encoding="utf-8") as handle: - handle.write(stripped + BASHRC_BLOCK) - return True + return _rc_block_upsert(path, BASHRC_MARK_BEGIN, BASHRC_MARK_END, BASHRC_BLOCK) def install_self(): home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai") print(op_binary("upgrade", home)) print(op_hook("install", home)) + # bash-hook always goes in, unconditionally, for back-compat -- it's harmless clutter + # in ~/.bashrc for a zsh/fish user who never touches bash. $SHELL additionally gets its + # own matching hook so the one-shot CLI bootstrap (--install) actually works out of the + # box on a non-bash login shell, not just when the user later runs /install zsh-hook. + login_shell = os.path.basename(os.environ.get("SHELL") or "") + rc = "~/.bashrc" + if login_shell == "zsh": + print(op_zsh_hook("install", home)) + rc = "~/.zshrc" + elif login_shell == "fish": + print(op_fish_hook("install", home)) + rc = "~/.config/fish/config.fish" print(op_venv("install", home)) - print("restart your shell or run: source ~/.bashrc") + print("restart your shell or run: source %s" % rc) return 0 diff --git a/test_tai.py b/test_tai.py index c43b974..b583d12 100644 --- a/test_tai.py +++ b/test_tai.py @@ -1618,7 +1618,10 @@ class InstallOpsTests(unittest.TestCase): def test_status_reports_all_targets(self): with mock.patch.object(tai, "container_engine", return_value=None): report = tai.install_report("status", list(tai.INSTALL_TARGETS), self.config.home, self.store) - for label in ("binary:", "bash-hook:", "venv:", "scheduler-service:", "telegram-service:", "container:", "vault:"): + for label in ( + "binary:", "bash-hook:", "zsh-hook:", "fish-hook:", "plexer-hook:", "zsh-plexer-hook:", + "fish-plexer-hook:", "venv:", "scheduler-service:", "telegram-service:", "container:", "vault:", + ): self.assertIn(label, report) self.assertIn("not installed", report) self.assertIn("never touch data", report) @@ -1647,6 +1650,91 @@ class InstallOpsTests(unittest.TestCase): self.assertTrue(os.path.isfile(path + ".bak-tai")) self.assertIn("not present", tai.op_hook("uninstall", self.config.home)) + def test_zsh_hook_install_remove_idempotent(self): + path = os.path.join(self.home, ".zshrc") + with mock.patch.object(tai, "shell_present", return_value=True): + self.assertIn("source ~/.zshrc", tai.op_zsh_hook("install", self.config.home)) + with open(path, encoding="utf-8") as handle: + content = handle.read() + self.assertIn(tai.BASHRC_MARK_BEGIN, content) + self.assertIn("command_not_found_handler", content) + self.assertNotIn("command_not_found_handle()", content) + self.assertIn("already present", tai.op_zsh_hook("install", self.config.home)) + self.assertIn("removed", tai.op_zsh_hook("uninstall", self.config.home)) + with open(path, encoding="utf-8") as handle: + self.assertNotIn(tai.BASHRC_MARK_BEGIN, handle.read()) + self.assertTrue(os.path.isfile(path + ".bak-tai")) + self.assertIn("not present", tai.op_zsh_hook("uninstall", self.config.home)) + + def test_fish_hook_install_remove_idempotent(self): + path = os.path.join(self.home, ".config", "fish", "config.fish") + with mock.patch.object(tai, "shell_present", return_value=True): + result = tai.op_fish_hook("install", self.config.home) + self.assertIn("installed", result) + with open(path, encoding="utf-8") as handle: + content = handle.read() + self.assertIn(tai.BASHRC_MARK_BEGIN, content) + self.assertIn("function fish_command_not_found", content) + self.assertIn("already present", tai.op_fish_hook("install", self.config.home)) + self.assertIn("removed", tai.op_fish_hook("uninstall", self.config.home)) + with open(path, encoding="utf-8") as handle: + self.assertNotIn(tai.BASHRC_MARK_BEGIN, handle.read()) + self.assertTrue(os.path.isfile(path + ".bak-tai")) + + def test_shell_hook_skips_loudly_when_shell_missing(self): + with mock.patch.object(tai, "shell_present", return_value=False): + self.assertEqual(tai.op_zsh_hook("install", self.config.home), "zsh-hook: zsh not found, skipped") + self.assertEqual(tai.op_fish_hook("upgrade", self.config.home), "fish-hook: fish not found, skipped") + self.assertEqual( + tai.op_zsh_plexer_hook("install", self.config.home), "zsh-plexer-hook: zsh not found, skipped", + ) + self.assertEqual( + tai.op_fish_plexer_hook("install", self.config.home), "fish-plexer-hook: fish not found, skipped", + ) + self.assertFalse(os.path.exists(os.path.join(self.home, ".zshrc"))) + self.assertFalse(os.path.exists(os.path.join(self.home, ".config", "fish", "config.fish"))) + + def test_shell_hook_uninstall_cleans_up_even_if_shell_missing_now(self): + with mock.patch.object(tai, "shell_present", return_value=True): + tai.op_zsh_hook("install", self.config.home) + with mock.patch.object(tai, "shell_present", return_value=False): + self.assertIn("removed", tai.op_zsh_hook("uninstall", self.config.home)) + + def test_zsh_plexer_hook_reuses_bash_syntax_different_file(self): + bash_path = os.path.join(self.home, ".bashrc") + zsh_path = os.path.join(self.home, ".zshrc") + with mock.patch.object(tai, "shell_present", return_value=True): + tai.op_plexer_hook("install", self.config.home) + tai.op_zsh_plexer_hook("install", self.config.home) + with open(bash_path, encoding="utf-8") as handle: + bash_content = handle.read() + with open(zsh_path, encoding="utf-8") as handle: + zsh_content = handle.read() + self.assertEqual(bash_content, zsh_content) + self.assertIn('case "$-" in', zsh_content) + + def test_fish_plexer_hook_uses_fish_syntax(self): + path = os.path.join(self.home, ".config", "fish", "config.fish") + with mock.patch.object(tai, "shell_present", return_value=True): + self.assertIn("installed", tai.op_fish_plexer_hook("install", self.config.home)) + with open(path, encoding="utf-8") as handle: + content = handle.read() + self.assertIn("status is-interactive", content) + self.assertIn("isatty stdin", content) + self.assertIn("isatty stdout", content) + self.assertIn("exec \"$HOME/.local/bin/tai.py\" --plexer-wrap", content) + self.assertNotIn("case \"$-\"", content) + self.assertNotIn("[ -z \"$TAI_PLEXER_ACTIVE\" ]", content) + + def test_install_self_detects_current_shell(self): + with mock.patch.object(tai, "shell_present", return_value=True): + with mock.patch.dict(os.environ, {"SHELL": "/usr/bin/zsh"}): + with mock.patch("builtins.print") as shown: + tai.install_self() + printed = "\n".join(str(call.args[0]) for call in shown.call_args_list) + self.assertIn("zsh-hook:", printed) + self.assertIn("source ~/.zshrc", printed) + def test_venv_kept_and_removed(self): folder = tai.venv_dir(self.config.home) python = os.path.join(folder, "bin", "python") @@ -2885,6 +2973,50 @@ class InstallTests(unittest.TestCase): self.assertIn("alias x=y", content) self.assertEqual(content.count(tai.BASHRC_MARK_BEGIN), 1) + def test_shell_rc_path_bash_zsh(self): + with mock.patch.dict(os.environ, {"HOME": "/home/x"}, clear=False): + self.assertEqual(tai.shell_rc_path("bash"), "/home/x/.bashrc") + self.assertEqual(tai.shell_rc_path("zsh"), "/home/x/.zshrc") + + def test_shell_rc_path_fish_respects_xdg_config_home(self): + with mock.patch.dict(os.environ, {"HOME": "/home/x"}, clear=False): + os.environ.pop("XDG_CONFIG_HOME", None) + self.assertEqual(tai.shell_rc_path("fish"), "/home/x/.config/fish/config.fish") + with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/custom/cfg"}): + self.assertEqual(tai.shell_rc_path("fish"), "/custom/cfg/fish/config.fish") + + def test_fish_hook_creates_missing_config_directory(self): + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "fish", "config.fish") + self.assertFalse(os.path.isdir(os.path.dirname(path))) + self.assertTrue(tai.upsert_fish_hook_block(path)) + with open(path, encoding="utf-8") as handle: + content = handle.read() + self.assertIn("function fish_command_not_found", content) + self.assertFalse(tai.upsert_fish_hook_block(path)) + + def test_zsh_fish_hook_roundtrip_preserves_other_content(self): + for shell, upsert, remove, present, needle in ( + ("zsh", tai.upsert_zsh_hook_block, tai.remove_zsh_hook_block, tai.zsh_hook_present, "command_not_found_handler"), + ("fish", tai.upsert_fish_hook_block, tai.remove_fish_hook_block, tai.fish_hook_present, "fish_command_not_found"), + ): + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "rc") + with open(path, "w", encoding="utf-8") as handle: + handle.write("# pre-existing user content\nexport FOO=bar\n") + self.assertTrue(upsert(path)) + self.assertTrue(present(path)) + with open(path, encoding="utf-8") as handle: + content = handle.read() + self.assertIn("export FOO=bar", content) + self.assertIn(needle, content) + self.assertTrue(remove(path)) + self.assertFalse(present(path)) + with open(path, encoding="utf-8") as handle: + content = handle.read() + self.assertIn("export FOO=bar", content) + self.assertNotIn(needle, content, "shell=%s" % shell) + def test_parser_prompt(self): args = tai.build_parser().parse_args(["what", "is", "(2+3)?"]) self.assertEqual(args.prompt, ["what", "is", "(2+3)?"]) diff --git a/tests/bashrc.py b/tests/bashrc.py index 0c200e9..3ba691e 100644 --- a/tests/bashrc.py +++ b/tests/bashrc.py @@ -52,6 +52,11 @@ class _BashrcHookTestBase(TestCase): mode = "pty" timeout = 30.0 + # Class-level defaults so a setup() that raises before these are assigned + # (e.g. the TAI_CONTAINER_TEST guard below) doesn't also crash teardown() + # with an unrelated AttributeError that masks the real failure reason. + tmp_home = None + _drain_task = None async def setup(self): if os.environ.get("TAI_CONTAINER_TEST") != "1": @@ -88,7 +93,323 @@ class _BashrcHookTestBase(TestCase): async def teardown(self): if self._drain_task is not None: self._drain_task.cancel() - shutil.rmtree(self.tmp_home, ignore_errors=True) + if self.tmp_home is not None: + shutil.rmtree(self.tmp_home, ignore_errors=True) + + +class _ShellPlexerHookTestBase(TestCase): + """Same fixture shape as _BashrcHookTestBase, generalized over shell: + installs the real plexer hook into a real $HOME/ for zsh or + fish and spawns the real shell binary. Subclasses set `shell` ("zsh" or + "fish") and `install_fn` (tai.op_zsh_plexer_hook / tai.op_fish_plexer_hook). + Needs that shell actually installed -- present in tests/docker/Dockerfile, + so setup() fails loud rather than silently skipping if it's missing, + same posture as TmuxControllerLiveTest's missing-tmux check.""" + + mode = "pty" + timeout = 30.0 + shell = None + install_fn = None + tmp_home = None + _drain_task = None + + async def setup(self): + if os.environ.get("TAI_CONTAINER_TEST") != "1": + raise TestAssertionFailed( + "refusing to run (TAI_CONTAINER_TEST is not set): this test installs a real " + "hook into a real shell rc file and must only ever run inside the disposable " + "test container (see tests/docker/Dockerfile), never against a real machine's home" + ) + if not shutil.which(self.shell): + raise TestAssertionFailed("%s is not installed -- required for the %s plexer-hook tests" % (self.shell, self.shell)) + self.tmp_home = tempfile.mkdtemp(prefix="tai-test-home-%s-" % self.shell) + os.makedirs(os.path.join(self.tmp_home, ".local", "bin"), exist_ok=True) + real_home = os.environ.get("HOME") + os.environ["HOME"] = self.tmp_home + try: + tai.op_binary("install", self.tmp_home) + self.install_fn("install", self.tmp_home) + finally: + if real_home is not None: + os.environ["HOME"] = real_home + else: + os.environ.pop("HOME", None) + self._drain_task = None + + async def _spawn_shell(self, args, mode=None, env_overrides=None): + env = self.build_env() + env.update(env_overrides or {}) + proc = ManagedProcess([self.shell] + list(args), mode=mode or self.mode, env=env, on_line=self._on_line) + await proc.start() + self._processes.append(proc) + return proc + + def _sock_matches(self): + return glob.glob(os.path.join(self.tmp_home, ".tai", "plexer", "*.sock")) + + async def teardown(self): + if self._drain_task is not None: + self._drain_task.cancel() + if self.tmp_home is not None: + shutil.rmtree(self.tmp_home, ignore_errors=True) + + +class ZshInstallAndAutoWrapTest(_ShellPlexerHookTestBase): + """Real `zsh -i`, with the real hook installed into a real $HOME/.zshrc, + auto-wraps into a real NativePlexerBackend -- the zsh counterpart of + BashrcInstallAndAutoWrapTest. The hook content is byte-identical to the + bash block (plain POSIX `[`/`case`, no bashism), only the target file + differs; this is the test that actually proves that claim rather than + just asserting it in a docstring.""" + + name = "zshrc-install-and-auto-wrap" + shell = "zsh" + install_fn = staticmethod(tai.op_zsh_plexer_hook) + + async def run(self): + proc = await self._spawn_shell(["-i"], env_overrides={"HOME": self.tmp_home}) + self._drain_task = asyncio.ensure_future(_drain_forever(proc)) + + sock_path = None + for _ in range(50): + matches = self._sock_matches() + if matches: + sock_path = matches[0] + break + await asyncio.sleep(0.2) + if not sock_path: + raise TestAssertionFailed( + "zsh -i never auto-wrapped into a plexer backend via the installed .zshrc hook" + ) + + controller = tai.NativePlexerController(sock_path) + await asyncio.sleep(0.3) + status = controller.status() + + controller.send_keys("echo $TAI_PLEXER_SOCKET", enter=True) + await asyncio.sleep(0.4) + pane = controller.read_pane(50) + + return {"sock_path": sock_path, "pane": pane, "status": status} + + async def verify(self, result): + if result["sock_path"] not in result["pane"]: + raise TestAssertionFailed( + "the zsh auto-wrapped inner shell's $TAI_PLEXER_SOCKET did not match the " + "backend's own socket path: %r" % result["pane"][-300:] + ) + if not result["status"].get("pid") or result["status"]["pid"] <= 0: + raise TestAssertionFailed("status() did not report a live child pid after zsh auto-wrap") + + +class ZshAntiRecursionTest(_ShellPlexerHookTestBase): + name = "zshrc-hook-anti-recursion" + shell = "zsh" + install_fn = staticmethod(tai.op_zsh_plexer_hook) + + async def run(self): + proc = await self._spawn_shell( + ["-i"], env_overrides={"HOME": self.tmp_home, "TAI_PLEXER_ACTIVE": "1"} + ) + await proc.send_line("echo anti_recursion_marker_ok") + await proc.wait_for_pattern(re.compile(r"anti_recursion_marker_ok"), timeout=8.0) + await asyncio.sleep(0.3) + return {"sock_matches": self._sock_matches()} + + async def verify(self, result): + if result["sock_matches"]: + raise TestAssertionFailed( + "a plexer socket was created even though TAI_PLEXER_ACTIVE was already set under zsh" + ) + + +class ZshTmuxActiveSkipTest(_ShellPlexerHookTestBase): + name = "zshrc-hook-tmux-active-skip" + shell = "zsh" + install_fn = staticmethod(tai.op_zsh_plexer_hook) + + async def run(self): + proc = await self._spawn_shell( + ["-i"], env_overrides={"HOME": self.tmp_home, "TMUX": "dummy-session:0.0"} + ) + await proc.send_line("echo tmux_skip_marker_ok") + await proc.wait_for_pattern(re.compile(r"tmux_skip_marker_ok"), timeout=8.0) + await asyncio.sleep(0.3) + return {"sock_matches": self._sock_matches()} + + async def verify(self, result): + if result["sock_matches"]: + raise TestAssertionFailed( + "a plexer socket was created even though $TMUX was already set under zsh" + ) + + +class ZshNonInteractiveSkipTest(_ShellPlexerHookTestBase): + """zsh counterpart of NonInteractiveSkipTest. Unlike bash, zsh's `-c` + does not source .zshrc at all by default either, so this explicitly + sources it, same technique as the bash version, to isolate the hook's + own `case $- in *i*)` guard (identical text to the bash block) from + zsh's separate default of not reading .zshrc for -c.""" + + name = "zshrc-hook-non-interactive-skip" + shell = "zsh" + install_fn = staticmethod(tai.op_zsh_plexer_hook) + mode = "pipe" + + async def run(self): + proc = await self._spawn_shell( + ["-c", 'source "$HOME/.zshrc" 2>&1; echo marker_non_interactive_ok'], + mode="pipe", + env_overrides={"HOME": self.tmp_home}, + ) + output = await proc.drain_until_eof(timeout=10.0) + returncode = await proc.wait(timeout=5.0) + return {"output": output, "returncode": returncode} + + async def verify(self, result): + if "marker_non_interactive_ok" not in result["output"]: + raise TestAssertionFailed( + "a non-interactive `zsh -c` that explicitly sourced the hook never reached past " + "it: %r" % result["output"][-500:] + ) + if result["returncode"] != 0: + raise TestAssertionFailed( + "non-interactive zsh exited %r after sourcing the hook" % result["returncode"] + ) + if self._sock_matches(): + raise TestAssertionFailed( + "a plexer socket was created from a non-interactive zsh -- the " + "`case $- in *i*)` guard failed" + ) + + +class FishInstallAndAutoWrapTest(_ShellPlexerHookTestBase): + """Real `fish -i`, with the real hook installed into a real + $HOME/.config/fish/config.fish, auto-wraps into a real NativePlexerBackend. + fish needs its own syntax entirely (no `[`/`case`/`$-`): `status + is-interactive`, `test`, `and`, and the `isatty` builtin instead of + `test -t`, which fish's `test` does not support -- this is the test that + proves that syntax actually works, not just that it looks plausible.""" + + name = "fish-config-install-and-auto-wrap" + shell = "fish" + install_fn = staticmethod(tai.op_fish_plexer_hook) + + async def run(self): + proc = await self._spawn_shell(["-i"], env_overrides={"HOME": self.tmp_home}) + self._drain_task = asyncio.ensure_future(_drain_forever(proc)) + + sock_path = None + for _ in range(50): + matches = self._sock_matches() + if matches: + sock_path = matches[0] + break + await asyncio.sleep(0.2) + if not sock_path: + raise TestAssertionFailed( + "fish -i never auto-wrapped into a plexer backend via the installed config.fish hook" + ) + + controller = tai.NativePlexerController(sock_path) + await asyncio.sleep(0.3) + status = controller.status() + + controller.send_keys("echo $TAI_PLEXER_SOCKET", enter=True) + await asyncio.sleep(0.4) + pane = controller.read_pane(50) + + return {"sock_path": sock_path, "pane": pane, "status": status} + + async def verify(self, result): + if result["sock_path"] not in result["pane"]: + raise TestAssertionFailed( + "the fish auto-wrapped inner shell's $TAI_PLEXER_SOCKET did not match the " + "backend's own socket path: %r" % result["pane"][-300:] + ) + if not result["status"].get("pid") or result["status"]["pid"] <= 0: + raise TestAssertionFailed("status() did not report a live child pid after fish auto-wrap") + + +class FishAntiRecursionTest(_ShellPlexerHookTestBase): + name = "fish-config-hook-anti-recursion" + shell = "fish" + install_fn = staticmethod(tai.op_fish_plexer_hook) + + async def run(self): + proc = await self._spawn_shell( + ["-i"], env_overrides={"HOME": self.tmp_home, "TAI_PLEXER_ACTIVE": "1"} + ) + await proc.send_line("echo anti_recursion_marker_ok") + await proc.wait_for_pattern(re.compile(r"anti_recursion_marker_ok"), timeout=8.0) + await asyncio.sleep(0.3) + return {"sock_matches": self._sock_matches()} + + async def verify(self, result): + if result["sock_matches"]: + raise TestAssertionFailed( + "a plexer socket was created even though TAI_PLEXER_ACTIVE was already set under fish" + ) + + +class FishTmuxActiveSkipTest(_ShellPlexerHookTestBase): + name = "fish-config-hook-tmux-active-skip" + shell = "fish" + install_fn = staticmethod(tai.op_fish_plexer_hook) + + async def run(self): + proc = await self._spawn_shell( + ["-i"], env_overrides={"HOME": self.tmp_home, "TMUX": "dummy-session:0.0"} + ) + await proc.send_line("echo tmux_skip_marker_ok") + await proc.wait_for_pattern(re.compile(r"tmux_skip_marker_ok"), timeout=8.0) + await asyncio.sleep(0.3) + return {"sock_matches": self._sock_matches()} + + async def verify(self, result): + if result["sock_matches"]: + raise TestAssertionFailed( + "a plexer socket was created even though $TMUX was already set under fish" + ) + + +class FishNonInteractiveSkipTest(_ShellPlexerHookTestBase): + """fish counterpart of NonInteractiveSkipTest. Unlike bash/zsh, fish + sources config.fish on *every* startup, interactive or not (per fish's + own docs) -- so this test does not need to explicitly source anything; + a plain `fish -c` already exercises the hook automatically, which makes + it the most direct possible test of the `status is-interactive` guard + actually being the thing keeping a non-interactive fish safe.""" + + name = "fish-config-hook-non-interactive-skip" + shell = "fish" + install_fn = staticmethod(tai.op_fish_plexer_hook) + mode = "pipe" + + async def run(self): + proc = await self._spawn_shell( + ["-c", "echo marker_non_interactive_ok"], + mode="pipe", + env_overrides={"HOME": self.tmp_home}, + ) + output = await proc.drain_until_eof(timeout=10.0) + returncode = await proc.wait(timeout=5.0) + return {"output": output, "returncode": returncode} + + async def verify(self, result): + if "marker_non_interactive_ok" not in result["output"]: + raise TestAssertionFailed( + "a non-interactive `fish -c` never reached past config.fish: %r" % result["output"][-500:] + ) + if result["returncode"] != 0: + raise TestAssertionFailed( + "non-interactive fish exited %r after sourcing config.fish" % result["returncode"] + ) + if self._sock_matches(): + raise TestAssertionFailed( + "a plexer socket was created from a non-interactive fish -- the " + "`status is-interactive` guard failed" + ) class BashrcInstallAndAutoWrapTest(_BashrcHookTestBase): @@ -376,6 +697,90 @@ class PlexerHookInstallUninstallRoundTripTest(TestCase): shutil.rmtree(self.tmp_dir, ignore_errors=True) +class DualHookSameFileIsolationTest(TestCase): + """bash and zsh both put the command-not-found hook and the plexer hook + into the *same* rc file (~/.bashrc / ~/.zshrc); fish puts both into the + same config.fish too. Each pair uses a distinct marker (BASHRC_MARK_* + vs PLEXER_BASHRC_MARK_*), but that's only a correctness claim until it's + proven the two blocks never clobber or duplicate each other when they + share one file -- which is exactly the failure mode the user asked never + to risk. Deterministic, no subprocess, safe to run anywhere.""" + + name = "shell-hook-dual-block-same-file-isolation" + timeout = 10.0 + + CASES = ( + ("bash", tai.upsert_bashrc_block, tai.remove_bashrc_block, tai.hook_present, + tai.upsert_plexer_bashrc_block, tai.remove_plexer_bashrc_block, tai.plexer_hook_present), + ("zsh", tai.upsert_zsh_hook_block, tai.remove_zsh_hook_block, tai.zsh_hook_present, + tai.upsert_zsh_plexer_block, tai.remove_zsh_plexer_block, tai.zsh_plexer_hook_present), + ("fish", tai.upsert_fish_hook_block, tai.remove_fish_hook_block, tai.fish_hook_present, + tai.upsert_fish_plexer_block, tai.remove_fish_plexer_block, tai.fish_plexer_hook_present), + ) + + async def setup(self): + self.tmp_dir = tempfile.mkdtemp(prefix="tai-test-dual-hook-") + + async def run(self): + outcomes = {} + for shell, up_a, rm_a, present_a, up_b, rm_b, present_b in self.CASES: + path = os.path.join(self.tmp_dir, shell + "rc") + with open(path, "w", encoding="utf-8") as handle: + handle.write("# pre-existing user content\nexport FOO=bar\n") + + up_a(path) + up_b(path) + with open(path, encoding="utf-8") as handle: + both_installed = handle.read() + both_present_after_install = (present_a(path), present_b(path)) + + rm_a(path) + with open(path, encoding="utf-8") as handle: + after_remove_a = handle.read() + presence_after_remove_a = (present_a(path), present_b(path)) + + up_a(path) + with open(path, encoding="utf-8") as handle: + after_reinstall_a = handle.read() + + rm_a(path) + rm_b(path) + with open(path, encoding="utf-8") as handle: + after_remove_both = handle.read() + + outcomes[shell] = { + "both_installed": both_installed, + "both_present_after_install": both_present_after_install, + "after_remove_a": after_remove_a, + "presence_after_remove_a": presence_after_remove_a, + "after_reinstall_a": after_reinstall_a, + "after_remove_both": after_remove_both, + } + return outcomes + + async def verify(self, result): + for shell, data in result.items(): + if data["both_present_after_install"] != (True, True): + raise TestAssertionFailed("%s: both hooks should be present after installing both: %r" % (shell, data["both_present_after_install"])) + if data["presence_after_remove_a"] != (False, True): + raise TestAssertionFailed( + "%s: removing hook A should leave hook B alone, got present=(A, B)=%r" % (shell, data["presence_after_remove_a"]) + ) + if data["after_remove_a"].count("export FOO=bar") != 1: + raise TestAssertionFailed("%s: removing hook A lost or duplicated pre-existing content" % shell) + if "export FOO=bar" not in data["after_remove_both"]: + raise TestAssertionFailed("%s: removing both hooks lost pre-existing content" % shell) + if data["after_remove_both"].strip() != "# pre-existing user content\nexport FOO=bar".strip(): + raise TestAssertionFailed( + "%s: file after removing both hooks has leftover marker text: %r" % (shell, data["after_remove_both"]) + ) + if data["after_reinstall_a"].count("export FOO=bar") != 1: + raise TestAssertionFailed("%s: reinstalling hook A duplicated pre-existing content" % shell) + + async def teardown(self): + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + ALL_CASES = [ BashrcInstallAndAutoWrapTest(), AntiRecursionTest(), @@ -383,6 +788,15 @@ ALL_CASES = [ NonInteractiveSkipTest(), TmuxControllerLiveTest(), PlexerHookInstallUninstallRoundTripTest(), + ZshInstallAndAutoWrapTest(), + ZshAntiRecursionTest(), + ZshTmuxActiveSkipTest(), + ZshNonInteractiveSkipTest(), + FishInstallAndAutoWrapTest(), + FishAntiRecursionTest(), + FishTmuxActiveSkipTest(), + FishNonInteractiveSkipTest(), + DualHookSameFileIsolationTest(), ] if __name__ == "__main__": diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile index 5a28216..81f15ef 100644 --- a/tests/docker/Dockerfile +++ b/tests/docker/Dockerfile @@ -7,12 +7,15 @@ # Claude Code. FROM python:3.11-slim -# bash: the thing being wrapped. tmux: TmuxPlexerController's real backend. -# procps: `ps`, used by tests/plexer.py and tests/bashrc.py to prove a -# process is actually gone, not just assumed gone. git: sysinfo/create_skill -# paths touch it; harmless to have for a "simulate everything" image. +# bash/zsh/fish: the shells being wrapped (tests/bashrc.py drives all three +# for real -- a real ` -i` against a real installed hook, not just the +# deterministic file-level checks that run outside the container too). +# tmux: TmuxPlexerController's real backend. procps: `ps`, used by +# tests/plexer.py and tests/bashrc.py to prove a process is actually gone, +# not just assumed gone. git: sysinfo/create_skill paths touch it; harmless +# to have for a "simulate everything" image. RUN apt-get update \ - && apt-get install -y --no-install-recommends bash tmux procps git ca-certificates \ + && apt-get install -y --no-install-recommends bash zsh fish tmux procps git ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/tests/rag.py b/tests/rag.py index a1bb8e1..d621bea 100644 --- a/tests/rag.py +++ b/tests/rag.py @@ -108,7 +108,13 @@ class RagIndexAndSearchTest(TestCase): name = "rag-index-and-search" mode = "pipe" - timeout = 60.0 * 8 # two LLM round trips per sampled file now (save + per-file search) + # Per file: one save round trip (up to SAVE_TIMEOUT, observed free-tier backends taking + # 30-60s even on success) + one search round trip (no LLM call, local FTS, cheap) + process + # wait/spawn overhead. This must scale with SAMPLE_SIZE (env-configurable, default 5) -- a + # flat constant sized for an earlier, smaller default silently became too tight and made a + # slow-but-successful free-tier response get killed as a false "timeout" failure. + SAVE_TIMEOUT = 180.0 + timeout = SAMPLE_SIZE * (SAVE_TIMEOUT + 10.0 + 15.0 + 15.0 + 10.0) + 60.0 async def setup(self): self.tmp_home = tempfile.mkdtemp(prefix="tai-test-home-") @@ -131,7 +137,7 @@ class RagIndexAndSearchTest(TestCase): % (title, sent_content) ) proc = await self.spawn(extra_args=["--yes", "--profile", "ragtest", prompt]) - await proc.drain_until_eof(timeout=90.0) + await proc.drain_until_eof(timeout=self.SAVE_TIMEOUT) await proc.wait(timeout=10.0) record = self._find_record_by_title(title)