README.md

June 24, 2026 · View on GitHub

 ███████╗██╗     ███████╗███████╗████████╗
 ██╔════╝██║     ██╔════╝██╔════╝╚══██╔══╝
 █████╗  ██║     █████╗  █████╗     ██║
 ██╔══╝  ██║     ██╔══╝  ██╔══╝     ██║
 ██║     ███████╗███████╗███████╗   ██║
 ╚═╝     ╚══════╝╚══════╝╚══════╝   ╚═╝
        [ p a r a l l e l   e x p e c t ]

CI License: MIT stryke

[EXPECT, BUT N SESSIONS AT ONCE]

"Tcl/Expect automated one terminal in 1990. This automates fifty, in parallel, from a playbook."

stryke-fleet is parallel expect/PTY automation as a pure-stryke package: transcripted sessions, declarative playbooks, a recipe corpus for the interactive CLIs everyone scripts by hand (ssh, sftp, telnet, sudo, su, passwd, psql, mysql, redis-cli, docker login, installers, network gear), and multi-host fan-out on stryke's thread pool. The PTY primitives (pty_spawn, pty_expect, pty_expect_table, …) and the parallelism (pmap) are stryke builtins — this package is the orchestration layer. No [ffi] table, no cdylib, no helper binary — just .stk modules loaded on use Fleet. Created by MenkeTechnologies.

strykelang · MenkeTechnologiesMeta · stryke-utils · stryke-mcpd

Read the Docs · Engineering Report


Table of Contents


[0x00] Why a Package, Not Core

The spawn/expect/send loop is already core: pty_spawn, pty_send, pty_read, pty_expect, pty_expect_table, pty_buffer, pty_alive, pty_eof, pty_close, pty_interact are stryke builtins, and pmap gives every stryke program a thread pool for free. What core deliberately does NOT ship is opinion: what a "step" is, what a failed run returns, what an ssh login chain looks like, how a transcript is shaped. That's policy, and policy belongs in a package where it can version independently:

  • Fleet::Session — transcripted sessions. Every send, match, timeout, and close is recorded, so a failed run hands you the full interaction log instead of a stuck process.
  • Fleet::Playbook — declarative step lists. +{expect, send, timeout, branches, optional} hashrefs run in order; first non-optional timeout fails the run with the step name.
  • Fleet::Recipes — the corpus. Login chains and prompt dances as pure data generators — composable, splice-able, unit-testable without a target host.
  • Fleet::Fanout — the part Tcl/Expect never had. One playbook, N targets, one PTY per thread, results in target order, wall-clock of the slowest host.

[0x01] Install

# From a release:
s pkg install -g github.com/MenkeTechnologies/stryke-fleet

# From a local checkout:
git clone https://github.com/MenkeTechnologies/stryke-fleet
cd stryke-fleet
s pkg install -g .              # installs into ~/.stryke/store/stryke-fleet@<version>/

# Or via Makefile:
make install

No cargo step. No cdylib. The installed store directory contains only stryke.toml + lib/*.stk — no compiled artifacts, fully portable across platforms.

[0x02] Quick Start

use Fleet

# One session, transcripted
val $s = Fleet::Session::open("ssh user\@host")
Fleet::Session::expect($s, qr/password:/, 10)
Fleet::Session::send($s, "$pw\n")
Fleet::Session::close($s)

# Declarative playbook — first non-optional timeout fails the run
val $r = Fleet::Playbook::run("ssh user\@host", [
    @{ Fleet::Recipes::ssh_login(+{ password => $pw }) },
    +{ send   => "uptime\n" },
    +{ expect => qr/load average/, timeout => 15, name => "uptime output" },
    +{ send   => "exit\n" },
])
p $r->{ok} ? "done" : "failed: $r->{error}"
p "$_->{event} $_->{data}" for @{ $r->{transcript} }

# Branch tables — first match wins, optional action coderef
+{ branches => [
       +{ re => qr/\(yes\/no\)\?/, do => fn { "yes\n" } },
       +{ re => qr/password:/ },
   ],
   send_matched => 1 }

# The headline: one playbook, fifty hosts, parallel PTY sessions
val $results = Fleet::Fanout::ssh([@hosts],
    Fleet::Recipes::ssh_login(+{ password => $pw }))
val $part = Fleet::Fanout::partition($results)
p "ok: #{len @{$part->{ok}}}  failed: #{len @{$part->{failed}}}"

[0x03] Sublibraries

#ModuleFileFnsHighlights
1Fleet::Sessionlib/Session.stk24open · send · send_line · send_lines (one line per send event) · send_secret (transmit but redact) · send_control (Ctrl-C/D/Z, pexpect sendcontrol) · expect · expect_exact (literal match, meta chars quoted) · expect_any (which of several prompts arrived) · sendline_expect (send-then-expect atom) · branch · read · drain (collect trailing output until idle) · buffer · alive · eof · interact · close · transcript · events · matches · last_match · timed_out · transcript_text
2Fleet::Playbooklib/Playbook.stk4validate · run (step lists with expect/send/branches/optional/send_matched/retries/retry_send/delay) · dry_run (pure step preview) · concat (explicit-form splice of step lists)
3Fleet::Recipeslib/Recipes.stk49ssh_login · ssh_copy_id · sudo · doas · su · psql · mysql · mariadb · pg_dump · redis_cli · mongo · influx · yes_to_all · cisco_enable · telnet_login · sftp · scp · ftp_login · passwd_change · docker_login · npm_login · gpg_decrypt · openssl_pem · rsync · ssh_keygen · git_clone · kinit · ldapsearch · restic · borg · age · smbclient · vault_login · aws_configure · htpasswd · keytool · ansible_vault · sqlplus · op_signin · ssh_add · unzip_encrypted · cqlsh · cryptsetup · clickhouse_client · gcloud_auth · heroku_login · pass_insert · bw_unlock · names
4Fleet::Fanoutlib/Fanout.stk18run (N targets, pmap) · batch (bounded concurrency, K at a time) · ssh (host-list convenience) · partition · count (total/ok/failed tally) · all_ok (boolean fleet-pass gate) · failed_cmds (cmd strings of failures) · ok_cmds (cmd strings of successes) · find (first result by cmd pattern) · pluck (project one key per result) · pluck_ok (project one key per success) · summarize (counts + error lines) · group_by (generic bucketing by a key fn) · group_by_error (bucket failures by error) · group_by_output (bucket successes by output — config-drift) · grep_transcript (which hosts printed X) · retry_failed (re-run only the failures) · retry_until (re-run failures up to N times)

Recipes are pure data — they return playbook arrayrefs and never spawn anything, so they compose with @{...} splices and unit-test without a network.

[0x04] What's NOT in Here

By design — these are stryke builtins, so we don't re-wrap them:

CategoryBuiltins (call directly)
PTY primitivespty_spawn · pty_send · pty_read · pty_expect · pty_expect_table · pty_buffer · pty_alive · pty_eof · pty_close · pty_interact
Parallelismpmap · pgrep · ppool and the rest of the parallel suite
Remote dispatchcluster([...]) + pmap_on — persistent SSH worker pools for non-interactive remote compute; Fleet is for sessions that prompt back
Method-form sugarPtyHandle class (require "perl_pty_class.stk" from strykelang examples)

If a function in this library can be replaced with one builtin call, it's a bug.

[0x05] CLI

s bin/fleet.stk expect "sh -c 'sleep 1; echo ready'" ready 10   # spawn, wait, print match
s bin/fleet.stk exchange cat hello hello 5                      # spawn, send, wait
s bin/fleet.stk recipes                                         # list the recipe corpus
s bin/fleet.stk version
s bin/fleet.stk help

bin/fleet.stk covers one-shot expect/send loops from the shell. Anything with branches, retries, or more than two steps belongs in a playbook script — see examples/.

[0x06] Tests

s test t/                       # assertions across every public function

t/test_fleet.stk is headless-CI safe by contract: every PTY assertion drives a local cat/sh — no network, no SSH, no target hosts (enforced by tests/repo-contract.sh). Recipes are asserted structurally as data.

[0x07] Layout

stryke-fleet/
├── stryke.toml                # pure-stryke package manifest (no [ffi])
├── Makefile                   # test / install / clean
├── LICENSE                    # MIT
├── lib/
│   ├── Fleet.stk              # `use Fleet` — pulls all four sublibs
│   ├── Session.stk            # `use Fleet::Session`  — transcripted PTY sessions
│   ├── Playbook.stk           # `use Fleet::Playbook` — declarative step runner
│   ├── Recipes.stk            # `use Fleet::Recipes`  — login/prompt corpus
│   └── Fanout.stk             # `use Fleet::Fanout`   — parallel multi-target runs
├── bin/
│   └── fleet.stk              # CLI front-end (one-shot expect/exchange)
├── t/
│   └── test_fleet.stk         # all-surface assertions (local processes only)
├── examples/
│   ├── local_demo.stk          # runnable anywhere — drives a local sh
│   ├── installer_autopilot.stk # yes_to_all against a fake installer
│   ├── parallel_ssh.stk        # the headline: recipe × N hosts, partitioned
│   └── orchestrate.stk         # all four layers: recipe + playbook + transcript + fanout
├── tests/                     # shell gate scripts (CI lints)
└── docs/                      # GitHub Pages site

[0xFF] License

MIT — see LICENSE.