// STRYKE-FLEET — ENGINEERING REPORT

Four sublibs · transcripted sessions · declarative playbooks · recipe corpus · parallel fan-out · pure stryke (no [ffi], no cdylib)

>_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.

4
sublibraries
95
public fns
0
cdylib / helper binaries
330
assertions in t/
4
runnable examples
22
CI gate scripts
24
Session fns
4
Playbook fns
49
Recipes fns
18
Fanout fns

~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.

SublibOwnsDoesn't own
Fleet::SessionOne 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::PlaybookStep 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::RecipesThe 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::FanoutOne 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:

PropertyConsequence
ComposableSplice 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 targetst/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.
AuditableA 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:

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.

AreaAssertion families covered
Sessionexpect/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.
Playbookvalidate 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.
RecipesStructural: 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.
FanoutThree 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.

BucketGates
repo-contractPure-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 gatesAt least one shields.io badge, at least one h2 section, at least one https link, ends with final newline.
polish gatesWorkflow 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:

  1. Add the fn Fleet::Recipes::name generator to lib/Recipes.stk with a leading ## docstring, returning a step arrayref. Add it to Fleet::Recipes::names.
  2. Assert its structure in t/test_fleet.stk: step count, payload shape, Playbook::validate acceptance, required-opt dies. No live target needed — recipes are data.
  3. 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

ItemValue
LicenseMIT
AuthorMenkeTechnologies
Repositorygithub.com/MenkeTechnologies/stryke-fleet
Parent languagestrykelang
Meta umbrellaMenkeTechnologiesMeta
Issuesgithub.com/MenkeTechnologies/stryke-fleet/issues