>_EXECUTIVE SUMMARY
stryke-fleet is the orchestration layer for interactive automation — the spawn/expect/send discipline Tcl/Expect defined in 1990, rebuilt on stryke's native PTY builtins and thread pool, with the one capability that lineage never grew: running N sessions in parallel from a single declarative playbook. Four sublibraries split the problem along its natural seams: Session (one transcripted PTY), Playbook (ordered step lists with branch tables and failure semantics), Recipes (login chains and prompt dances as pure data), Fanout (one playbook × N targets on pmap).
The package is intentionally thin: every blocking, polling, regex-matching, PTY-allocating concern lives in strykelang core (perl_pty.rs), where it's threaded, tested, and fast. What lives here is policy — what a step is, what a failed run returns, what an ssh login looks like — the part that should version independently of the language.
~SUBLIBRARY BOUNDARIES
Each sublibrary owns one layer of the stack. Nothing reaches around its neighbor: Fanout only calls Playbook, Playbook only calls Session, Session only calls the pty_* builtins.
| Sublib | Owns | Doesn't own |
|---|---|---|
Fleet::Session | One PTY with a memory: every send, match, timeout, and close is appended to an ordered transcript, so a failed run hands back the full interaction log instead of a stuck process. Transcript readers (events, matches, last_match, timed_out, transcript_text) and the newline-guaranteeing send_line round out the surface. | Blocking, polling, regex compilation, PTY allocation — all in the pty_* builtins. Session adds bookkeeping, nothing else. |
Fleet::Playbook | Step semantics: ordered +{expect, send, branches, timeout, optional, send_matched, retries, retry_send, delay} hashrefs; validate rejects malformed steps before anything spawns; first non-optional timeout fails the run with the step's name; dry_run previews the step list without spawning. | Session lifecycle when handed an already-open session (caller closes); parallelism (Fanout's job). |
Fleet::Recipes | The corpus (48 generators + names): logins (ssh_login, ssh_copy_id, telnet_login, ftp_login, docker_login, npm_login, heroku_login, vault_login, gcloud_auth, op_signin, bw_unlock), copies (scp, sftp, rsync), privilege (sudo, doas, su, passwd_change, cisco_enable), DB shells (psql, pg_dump, mysql, mariadb, mongo, redis_cli, influx, cqlsh, clickhouse_client, sqlplus), crypto/secrets (gpg_decrypt, openssl_pem, ssh_keygen, ssh_add, age, cryptsetup, keytool, htpasswd, ansible_vault, pass_insert, unzip_encrypted), backups (restic, borg), installers (yes_to_all), VCS (git_clone), auth/dir (kinit, ldapsearch, aws_configure), file shares (smbclient) as step-list generators. Pure data — nothing spawns, so every recipe unit-tests without a network. | Execution. A recipe that calls pty_spawn is a bug; recipes return arrayrefs and stop. |
Fleet::Fanout | One playbook × N targets: one PTY per pmap thread, results in target order. Dispatch: run (all at once), batch (bounded concurrency, K at a time), ssh (host-list convenience). Triage: partition (ok/failed split), count/all_ok (tallies and the boolean fleet gate), summarize (counts + error lines). Slicing: failed_cmds/ok_cmds (target lists), find (first result by cmd pattern), pluck/pluck_ok (project one value per result), group_by/group_by_error/group_by_output (bucket by a key fn, by error, by output for config-drift), grep_transcript (which hosts printed X). Recovery: retry_failed (one re-run of the failures), retry_until (re-run each failure up to N times). | Non-interactive remote compute — that's core's cluster([...]) + pmap_on persistent SSH worker pools. Fleet is for sessions that prompt back. |
$PURE-DATA RECIPE CONTRACT
Recipes return playbook arrayrefs and never spawn. This single constraint buys three properties:
| Property | Consequence |
|---|---|
| Composable | Splice any recipe into a larger playbook with @{ Fleet::Recipes::ssh_login(...) }, then keep appending steps. Recipes chain: ssh_login + sudo + psql is one flat step list. |
| Testable without targets | t/test_fleet.stk asserts recipe structure (step counts, send payloads, validity under Playbook::validate) with zero hosts, zero containers, zero network. The corpus can grow recipes for hardware nobody in CI owns. |
| Auditable | A recipe is printable. to_json a step list to see exactly what would be sent before pointing it at a production switch. |
/WHY A PACKAGE, NOT CORE
The split follows the mechanism/policy line:
- Core owns mechanism.
pty_spawn/pty_expect/pty_expect_tableare Rust (perl_pty.rs): PTY allocation, non-blocking reads, byte-regex matching against a live buffer, timeout arithmetic. Performance-critical, platform-touching, rarely changing. - The package owns policy. Step shapes, failure semantics, transcript format, the recipe corpus. These evolve with usage — a new step key or recipe ships as a package version bump, not a language release.
- The corpus compounds. Recipes are where the long tail lives (network gear, vendor installers, database CLIs). A package can absorb contributions at corpus speed; a language can't and shouldn't.
Pure stryke, deliberately: the install is a tarball of five .stk files. Same source as runtime — debug a playbook by reading the installed file. No per-triple artifacts, no FFI unwind, nothing to build.
#TEST SURFACE
t/test_fleet.stk uses stryke's native assertions (assert_eq, assert_ok, assert_match, assert_dies) and exits TAP-style via test_run. Headless-CI safe by contract — tests/repo-contract.sh greps the test file and fails CI if it ever spawns ssh.
| Area | Assertion families covered |
|---|---|
| Session | expect/send round trip against local cat; liveness; transcript ordering (send → match → close); branch table picks the matching arm and runs its action coderef; send_line newline guarantee, events/matches/last_match transcript reads, timed_out after a forced timeout, transcript_text newline-escaped dump. |
| Playbook | validate rejects non-arrayrefs, actionless steps, scalar branches, negative retries/delay; ok path runs all steps and records cmd + transcript; non-optional timeout fails with step name and index; optional timeout continues; send_matched echoes the branch result back; retries + retry_send nudge a timed-out expect into a match; delay paces a step (ms clock); dry_run previews without spawning. |
| Recipes | Structural: step counts with/without passwords, password/passphrase payload shape, ordering (ftp name-then-password, npm email last, openssl verify re-send), required-opt dies (sudo, doas, cisco_enable, scp, gpg_decrypt, ftp_login, openssl_pem, npm_login), every recipe validates under Playbook::validate, roster pinned at 48 via names. |
| Fanout | Three local sessions in parallel, results in target order, all ok; failure partition (both timeout → both in failed); scalar cmds rejected; batch bounded concurrency returns the flat list in order; count/all_ok tallies and the empty-set vacuous-pass; summarize counts + error-line format; failed_cmds/ok_cmds target lists; find by cmd pattern; pluck/pluck_ok projections; group_by/group_by_error/group_by_output bucketing; grep_transcript output search; retry_failed re-runs only failures, tags them, leaves ok entries untouched; retry_until per-target attempt cap with attempts tag. |
%CI GATES
22 shell gate scripts in tests/ are wired into .github/workflows/ci.yml, plus a stryke job that installs the latest strykelang release binary, syntax-checks every .stk file, installs the package into the global store, and runs s test t/. Every gate is reproducible locally with bash tests/<gate>.sh.
| Bucket | Gates |
|---|---|
| repo-contract | Pure-stryke (no Cargo.toml, no [ffi]). lib/ has exactly 5 files. Every sublib declares package Fleet::<Name>. Examples shebang. bin/fleet.stk shebang. Headless guard: t/ must not spawn ssh. Makefile has test/install/clean. LICENSE is MIT. |
| docs gates | <h1> present, <body> tag, closing </html>, no deprecated tags, no inline event handlers, no placeholder hrefs, no plaintext http://, target="_blank" always paired with rel="noopener noreferrer". |
| README gates | At least one shields.io badge, at least one h2 section, at least one https link, ends with final newline. |
| polish gates | Workflow files have no tabs (YAML), every tests/*.sh has +x and a bash shebang, man pages (if any) have synopsis section + final newline + no trailing whitespace. |
!EXTENDING
Adding a recipe means three edits:
- Add the
fn Fleet::Recipes::namegenerator tolib/Recipes.stkwith a leading##docstring, returning a step arrayref. Add it toFleet::Recipes::names. - Assert its structure in
t/test_fleet.stk: step count, payload shape,Playbook::validateacceptance, required-opt dies. No live target needed — recipes are data. - Mention it in the README sublib table and
docs/index.html.
Adding a step key (a new behavior in the runner) is a Playbook::run change: implement, document in the lib header, and pin the behavior with both an ok-path and a failure-path assertion.
@PROJECT METADATA
| Item | Value |
|---|---|
| License | MIT |
| Author | MenkeTechnologies |
| Repository | github.com/MenkeTechnologies/stryke-fleet |
| Parent language | strykelang |
| Meta umbrella | MenkeTechnologiesMeta |
| Issues | github.com/MenkeTechnologies/stryke-fleet/issues |