Skip to content

Running an operation ​

Driving an operation interactively: sidecar mode, the action bar, verification, sessions and resume.

Sidecar mode (default) ​

Sidecar is the default run mode (-m sidecar). Samaritan acts as your copilot: it displays each step's resolved command, you run it yourself in your own terminal, and samaritan validates step.expect only when you press [v]. No commands are ever sent automatically.

bash
# Default — sidecar mode
samaritan run deployment.yaml --env staging

# Attach to an existing tmux pane so [v] can read its output
samaritan run deployment.yaml --env staging --attach mysession:0.0

# Mid-flight: press [t] at any step to attach a pane

Pressing [t] fires immediately (no Enter needed) and shows a numbered picker of all existing tmux panes — pick one by number, or type a raw target (mysession:0.0, %3). Samaritan's own pane is marked (this pane — samaritan):

    Available tmux panes:
      1) work:0.0  zsh
      2) work:0.1  node  (this pane — samaritan)
    Select pane [number or target, Enter to cancel]:

How sidecar attach works:

ScenarioWhat happens
sessions: defined in YAMLSamaritan bootstraps its own tmux session; prints Attach with: tmux attach -t samaritan-<id>
--attach <target> flagSamaritan attaches a pipe-pane capture to your existing pane without touching your session
No sessions, no --attachPrompt-only mode; [v] will indicate that you need [t] to attach a pane first

Caveats:

  • --attach mode uses a single capture target for all steps. It takes precedence over sessions:: the named sessions are not spawned, per-step session: routing is ignored, and every step's [v]/[p] uses the attached pane. Samaritan prints a one-line notice when the operation declares sessions:. Press [t] to point a step at a different pane (the new pane is used from then on). resume keeps the same precedence: it re-attaches the saved pane and does not spawn sessions:. To get one pane per named session, run without --attach and samaritan spawns them.
  • tmux allows one pipe-pane per pane, so attaching replaces any existing pipe on it. Run one samaritan per pane. If a second run attaches to the same pane anyway:
    • it warns at attach (This pane was captured by samaritan session <id> (pid N))
    • the first run notices at its next step or [v]/[e], prints Capture pipe on … was taken over by …, and re-attaches
    • a verify whose pipe missed part of the step reads the pane scrollback instead of reporting missing: for output that is on screen: from the line [p] pasted the command on, or, when the command was typed rather than pasted, from its last echo (which may include an earlier attempt)
    • a run only closes the pipe on exit while it still owns it, so one run exiting never cuts the other's capture
  • [v] with nothing captured since the step began prints a hint to run the command in the pane first.
  • After [p], [v] judges only output from that paste on, so output left by an earlier attempt of the same step (a retry, or a resumed run from another day) cannot trip a check such as not_contains. A new step or a [t] pane switch starts without a paste.
  • samaritan never kills your session on exit — only the temporary capture pipe and temp file are cleaned up.

Step display in sidecar:

  • type: automatic steps: Command is displayed prominently; the command_displayed event is written to the audit log (not command_sent). The report renders it as **Command (run by operator)**.
  • type: manual steps: Same prompt loop as always.
  • Each step is resolved with its variants.<env> overrides before it is shown, so the command, instruction, [p] paste and [v] verify match generate manual --env <env> exactly.
  • Script-only steps (script: with no inline command): the run loop shows Script: <path>, embeds the script file's content, and displays the bash <path> invocation to run. [c] copy and [p] send to pane operate on that bash <path> invocation.
  • Both types offer [v] verify (when expect is defined), [t] attach pane, and [p] send to pane (when the step has a command or a script and a pane is attached).

Execution flow (spawn-own sessions) ​

samaritan run deployment-with-sessions.yaml --env production
  1. Creates a tmux session (samaritan-<id>)
  2. Bootstraps one window per named session (local or SSH)
  3. Starts pipe-pane background capture on every pane
  4. Launches the interactive TUI in the current terminal
  5. Writes all events to /tmp/samaritan-<id>.jsonl

In iTerm2, panes open as native vertical splits automatically. In any other terminal with tmux, panes are split inside the current window.

Interactive prompts ​

At each step, SAMARITAN pauses and shows the step details, then prompts based on step type.

Each step heading uses the same number as the single-environment manual (generate manual --env <name>), followed by the step's position in the run. Below the heading you get the parent section (for a sub-step) and what comes next:

[Step 10.2 · 34/46] MANUAL: review-upload-and-record-backup
    Section  : Step 10: upload-and-backup
    Next     : Step 10.3: verify-backup (12 remaining)
  • Step 10.2 is the number in generate manual --env <name>: sub-steps are dotted (10.1, 10.2, 10.2.1). It is not the letter scheme (10a, 10b1) that the multi-environment manual uses.
  • 34/46 is the position in the run's flat step list (parents and sub-steps all count). Use this number with [b] back / [j] jump and --from-step.
  • remaining counts the steps still to visit. Steps that when: excludes for this environment are not counted.
Step type / modePromptKeys
automatic in sidecarCommand — [p] pastes it into <pane>: (pane attached) or Run this command in your terminal ([t] attaches a pane for [p]/[v]):Bar: y=run it (after p), Enter=done, p=paste, v=verify, t=attach (no pane yet), s=skip, r=rollback, ?=more. Under ?: c=copy, n=note, e=evidence, x=remove evidence, t=attach another pane, b=back, j=jump, g=global rollback, q/abort=quit
automatic (tmux-backed, non-sidecar)▶ Send to tmux?Enter=send, s=skip, r=rollback, g=global rollback, q=quit
automatic (prompt-only, non-sidecar)▶ Execute?Enter=confirm, s=skip, r=rollback, g=global rollback, q=quit
manual✋ Mark doneSame compact bar: Enter/notes=confirm, v=verify, p/y/t (sidecar), s=skip, r=rollback, ?=more (n=note, e=evidence, b=back, j=jump, g=global rollback, q/abort=quit)
approval⚡ approve/rejectapprove, reject, r=rollback, g=global rollback, skip

The step bar is compact. It shows the keys of the usual flow ([p] → [y] → ↵, [v]), plus [s], [r] and [?] more. [t] is shown while no pane is attached. ? prints the rest once (copy, note, evidence, back, jump, global rollback, abort…). Every key works whether it is shown or not, so n, e, b and j need no ? first.

g=global rollback is only offered when the operation declares a top-level rollback: block. It runs the consolidated recovery (see Group per-step rollbacks into the global rollback) and then aborts.

After the last step, when anything can be rolled back, the run shows All steps done with ↵ finish · r roll back a step · g global rollback, so finishing the final step never takes rollback away.

The global-rollback confirmation asks [y/N]. On a TTY it is answered with a single key, without Enter.

Two guards keep a stray Enter from completing a step on a TTY:

  • Keys typed after an action key in one burst are discarded. Typing v⏎ quickly runs [v] only; the Enter does not mark the step done.
  • A second ↵ within 400 ms of one that was acted on, with no other key in between, is ignored, with ↩ Enter ignored: pressed twice within 400 ms. A double press on the step you just finished never completes the step that opened next. Press ↵ again to confirm.

Before each step, samaritan prints Expected: <criteria> up front (e.g. Expected: contains: Running, does not contain: CrashLoopBackOff) so you know what [v] will check before you run anything.

Verify output: checklist, highlighting, and the line-number gutter ​

Pressing [v] runs every check in step.expect (no short-circuit on the first failure) and renders a PASS/FAIL header, a per-check checklist, and the captured output — on both pass and fail:

    ✅ PASS
    ✅ contains: Running
    ✅ does not contain: CrashLoopBackOff
    ✅ does not contain: Error
    ✅ at least 1 line(s) (2 lines, expected ≥ 1)
  ╭─ output (tail) ────────────────────────────────────────╮
  │ 1  │ NAME           READY   STATUS    RESTARTS   AGE   │
  │ 2 →│ web-server-0   1/1     Running   0          45s   │
  ╰───────────────────────────────────────────────────────╯
  • Checklist — one line per check (✅/❌), in the same order as Expected:. Numeric/count checks show their computed value inline, e.g. found "2", need ≥ 3 or 2 lines, expected ≥ 1.
  • Highlighted output — the captured output (tail of the last 12 lines by default, or full output after [m]) is shown with:
    • green + inverse around the text that satisfied a passing contains / any_line_contains / matches / any_line_matches check
    • red around the offending text that violated a failing not_contains / no_line_contains / no_line_matches check
    • missing: <expected> for failing checks whose expected text isn't present in the output at all
  • Line-number gutter — each line of the output block is prefixed with its absolute line number (accounting for tail truncation) and a → arrow on any line containing a highlight, e.g. 2 →│ web-server-0 1/1 Running 0 45s. This makes it easy to see exactly which line satisfied (or violated) a check.

On FAIL, a single-key menu follows:

    ❌ FAIL
    ❌ contains: Running
    ❌ does not contain: CrashLoopBackOff
  ╭─ output (tail) ──────────────────────────────────────────────╮
  │ 1  │ NAME           READY   STATUS             RESTARTS   AGE │
  │ 2 →│ web-server-0   0/1     CrashLoopBackOff   3          2m  │
  │ 3  │ missing: Running                                         │
  ╰─────────────────────────────────────────────────────────────╯
⚠️  Assertion failed. [y=re-run / o=override with reason / r=rollback / c=copy command / m=more / v=re-verify / Enter=stop]:

Each choice fires on a single key press (no Enter needed).

  • y — re-run (sidecar, with a pane attached): paste the step's command into the pane again, press the same guarded Enter as [y] run it, wait for the command to finish, and verify again. Use it when the check waits on a state change — for example a rollout that still reports fewer ready replicas than desired. It is one key instead of Enter=stop → [p] → [y]. Each run is its own send_to_pane + enter_sent audit event. If the paste can't be armed (the command isn't at the cursor) or Enter is refused, nothing is run and you're back on the step bar. For a resource that takes minutes, prefer a command that waits by itself (for example kubectl rollout status deployment/web-server --timeout=10m) so one run is enough.
  • o — record an override reason in the audit log and continue
  • r — trigger rollback
  • c — copy command: copy the step's ${VAR}-resolved command to the clipboard so you can paste and re-run it, then the menu re-renders (copy is not a terminal choice). Only shown when the step has a command.
  • m — more: re-render the same captured output in full (output (full), starting the gutter at line 1) instead of just the tail
  • v — re-verify: re-capture the pane output and re-run all checks (useful when the command is still finishing)
  • Enter — stop

On PASS, samaritan prints ✅ Verify passed. The step is not done yet: press ↵ to mark it done, or [v] to re-check. A pass never completes the step by itself: ↵ does, and you can re-run [v] as often as you like.

Verification runs against cleaned pane output: ANSI color codes and escape sequences are stripped and \r-overwrites (progress bars, \r\n line endings) are resolved before expect assertions run — so contains/equals match what you actually see on screen, even when tools colorize their output.

Responsive output: command boxes and the verify output are sized to your terminal. On a narrow terminal, over-long lines are truncated with … so box borders stay aligned, and the captured-output tail shrinks to fit a short window. When output is piped or redirected (no TTY), rendering is unbounded and full content is preserved.

Manual-step actions: note, evidence, verify ​

manual steps offer extra operator actions while you're working the step — they don't complete the step, so you can use any of them as many times as you like before pressing Enter to mark the step done. The bar shows the common keys; ? lists the rest:

[y] run it (after [p])  ·  [↵] done  ·  [p] paste  ·  [v] verify  ·  [s] skip  ·  [r] rollback  ·  [?] more
> ?
[c] copy  ·  [n] note  ·  [e] evidence  ·  [x] remove evidence  ·  [t] attach another pane  ·  [b] back  ·  [j] jump  ·  [g] global rollback  ·  [abort] abort
  • [n] note — record a free-text annotation (e.g. "restarted pod manually, confirmed with on-call"). Stored in the JSONL audit log as a user_input/note event and rendered as a bullet list under the step in the --report Markdown.

  • [e] evidence — capture and persist evidence with the session, the same way you'd attach a file in Claude Code. You're offered up to three sources:

    • capture terminal output (default, only offered when a tmux session is attached to the step) — grabs everything written to the pane since the step started and stores it as command_output evidence
    • [f] file or image — drag a file into the terminal (most terminals insert its path) or type/paste the path; SAMARITAN reads it from disk, copies it into ~/.samaritan/sessions/<session-id>/evidence/, and stores it as a typed EvidenceItem (screenshot for png/jpg/jpeg/webp/gif, video for mp4/webm/mov, file for everything else)
    • [t] type/paste text — type or paste evidence content directly

    Every captured item can include an optional description, and is rendered in the --report Markdown — screenshots as embedded images (![Evidence](path)), files/videos as download-style links ([View file](path)), and text/terminal captures as fenced code blocks.

    All evidence bytes — including dragged-in files, screenshots, and videos — are stored exclusively under ~/.samaritan/sessions/<session-id>/evidence/, alongside the session's own JSON record. SAMARITAN never leaves a second copy elsewhere: the persisted session references the file by path rather than embedding its raw bytes.

  • [x] remove evidence — only offered once at least one item has been captured for the current step. Lists the step's captured evidence (type, description, and stored path), lets you pick one by number to delete, removes it from the session record, and — for file/screenshot/video evidence copied into the session's evidence directory — deletes the copy from disk too (your original source file is never touched). Recorded in the JSONL audit log as an evidence_removed event, and the --report Markdown omits removed items entirely.

  • A key that does not apply never marks the step done. [v] on a step without expect, [p] on a step without a command, [x] with no evidence, [b] on the first step and so on print why ([v] — this step has no expect, so there is nothing to verify ([e] captures evidence). The step is not marked done (↵ does that).) and keep you on the bar. Only ↵ (or typed notes) completes a step.

  • ↵ with nothing run asks first (sidecar, pane attached). When the step has a command, nothing was pasted with [p], and the pane has printed nothing since the step started, ↵ asks Nothing was pasted or run in <pane> since this step started. Mark done anyway? [y / n=go back]. This catches a stray Enter that lands on the next step after a verify pass. A command you type into the pane yourself produces output, so it never asks. y is logged (user_input/done_without_run) and shows as a ⚠️ note on the step in report.md.

  • [v] verify — only offered when the step defines expect. Reads the pane output captured since the step started and asserts it against expect (the same assertOutputDetailed/interpolateExpect machinery automatic steps use), evaluating every check (not just the first failure) and rendering the PASS/FAIL checklist + highlighted, line-numbered output described in Verify output: checklist, highlighting, and the line-number gutter — and, on failure, the override/rollback/[m] more/[v] re-verify/stop prompt. This is what actually checks expect on manual steps; without pressing [v], a manual step's expect is documentation only.

    • Auto-capture on pass (closes the expect ↔ evidence loop): the first time a step's [v] verify passes, the verified pane output is automatically saved as a command_output evidence item (marked automatic/validated, source: verify) — so the output you checked also becomes the output recorded in the session and --report. The evidence is captured from the pane's rendered screen (tmux capture-pane -p -J), so it reads as clean text in the report instead of the raw terminal byte stream (no ANSI/cursor-move/redraw noise). Re-pressing [v] won't record duplicates. evidence (the record) and expect (the check) stay separate concepts; this just records what you verified.
  • [r] rollback — runs this step's rollback and stays on the step. In sidecar/manual runs it is operator-driven: each ${VAR}-resolved command is shown, [p] pastes it into the attached pane, [v] verifies its expect, ↵ confirms you ran it (see Running a rollback); a non-sidecar run with spawn-own sessions sends it via tmux. If this step has no rollback, it offers the nearest upstream step's rollback instead — following the needs chain if present, otherwise scanning earlier steps in document order (only completed steps are offered).

  • [g] global rollback — only offered when the operation declares a top-level rollback: block. Previews the consolidated recovery — the explicit rollback.steps plus, when aggregate_step_rollbacks is on, every completed step's own rollback in reverse order (ordered by reverse-topological order when needs are present) — asks for a single-key y confirmation, runs it the same way as [r], then aborts the operation (the session stays resumable). Use it when a failure means abandoning forward progress and unwinding what's been done so far.

  • Forward gating (needs) — before starting a step whose needs aren't all completed (e.g. a dependency was skipped by [j] jump or --from-step), the run loop warns (⚠️ This step needs: …) and prompts Start anyway? [y=proceed / Enter=go back]. y proceeds and records a needs_override audit event; Enter rewinds to the first unmet dependency. A skipped dependency counts as unmet.

  • [p] send to pane (sidecar only) — pastes the step's ${VAR}-resolved command into the attached tmux pane without pressing Enter, so you review it at your own prompt and run it yourself. The command is delivered via a tmux paste buffer with bracketed paste (paste-buffer -p), so a multi-line command (e.g. a heredoc script) lands as one atomic block at your prompt and does not execute line-by-line — you press Enter once yourself to run it. Only offered when the step has a command and a pane is attached (a spawn-own sessions: pane, or one attached via [t]/--attach); with nothing attached it tells you to [t] attach first. Before pasting, line endings are normalized: CRLF or a lone CR becomes LF, and trailing newlines are stripped. tmux rewrites every LF to CR when it pastes. A stray \r, for example from a CRLF variable value, would therefore reach the shell as a blank line after each \ continuation and split the block into separate commands. A trailing newline would act as Enter in a shell without bracketed paste. Multi-line commands paste straight away, with no confirmation. They land as one un-run block because the pane's shell supports bracketed paste (on by default in bash ≥ 4.4 and zsh ≥ 5.1). Don't use [p] for a multi-line command in a sh/dash pane, or with bracketed paste turned off: there each line runs as it arrives. In bash, an empty row after a line that exactly fills the pane width is a readline redraw artifact. The pasted bytes are unchanged. To re-run a command during verify, just press [p] again, then [v]. Logged as a user_input/send_to_pane breadcrumb in the JSONL audit log. [p] itself never executes anything; [y] (below) is the one explicit way to have samaritan press Enter.

    • An un-run paste is tracked. Samaritan reads the pane's cursor line. While it still ends with the pasted command, the command has not run:
      • [p] is refused (Nothing pasted.) until you run or clear it (Ctrl+U), so a new paste never lands on the end of an old one (echo aecho b).
      • ↵ on that step asks Mark done anyway? [y / n=go back]. y is logged (user_input/paste_not_run) and shows as a ⚠️ note on the step in report.md.
      • Entering a later step warns when an earlier step's paste is still on the pane's input line.
      • Rollback p and ↵ follow the same rules.
  • [y] run it (Enter in pane) (sidecar only, shown right after a [p] paste) — samaritan presses Enter in the attached pane for you, so you don't have to switch panes. It sends Enter only while the pane is exactly as the paste left it. Right after [p], samaritan waits for the paste to finish echoing, then checks the command is sitting at the cursor (otherwise [y] isn't offered). When you press [y] it re-reads the pane and refuses with 🛑 Enter NOT sent: <reason> if any of these changed:

    CheckWhy
    Same pane (#{pane_id}), still aliveEnter must go to the pane that holds the paste
    Not in copy/view modeEnter would copy a selection instead of running the command
    synchronize-panes offEnter would reach every pane in the window
    Input not disabled (select-pane -d)The key would be dropped
    Same foreground program (#{pane_current_command})Something else (vim, less, ssh…) took over the pane
    No pane output since the paste, cursor and prompt row unchangedNothing was typed or edited, and the command hasn't already run, so it can't run twice

    After Enter, samaritan waits for the command and verifies it for you. It shows ⏳ Running… verify follows when it finishes. Once the pane is back at the same bare prompt it showed before the paste (digits such as clocks or exit codes may differ) and output has been quiet for 0.3s, it prints ✔ Finished. and runs [v] against the step's expect. A step without expect just reports it finished. A slow, silent program in the foreground (e.g. kubectl rollout status) keeps it waiting. If the prompt can't be recognised (a cd changed it), 3s of quiet with the shell back in front counts as finished. Any key stops waiting and hands the action bar back; press [v] yourself when the command is done. The usual step flow is [p] → glance at the pane → [y] → ↵.

    A refusal sends nothing and keeps [y] available (leave copy-mode, press [y] again). If the pane changed, press Enter there yourself, or [p] to paste again. [y] only ever applies to the paste made from the same step (or rollback entry). It isn't carried over to another step, or to the same step revisited with [b]. Logged as user_input/enter_sent, and the report marks the command ⏎ Enter sent from samaritan [y]. There is no flag or setting for it.

  • [b] back — go back to an earlier, already-processed step to re-run it (e.g. an external dependency was fixed and you want to retry from there). Shows a numbered picker of the prior steps (type the listed number, the manual's step number such as 2.7, or a unique part of the step name); the chosen step and every step after it are reset to pending and execution resumes from there. The append-only JSONL audit log keeps the full history of the earlier attempt, and the rewound step index is persisted so resume stays consistent. Not offered on the first step.

    • Re-run only one step. When there are steps between the one you pick and the one you are on, [b] asks Re-run only "<picked>", then come back to "<current>"? [y / n=rewind].
      • y resets only the picked step. Its header shows Next : back to step N. Once it is done (or skipped), the run returns straight to where you were, and the steps in between keep their status: they are neither re-run nor marked skipped.
      • n rewinds as described above.
      • Logged as user_input rerun / rerun_return. If you abort during the re-run, resume continues after the re-run step, not at the step you came from.
      • Prefer this over [b] followed by [j]: a [j] back over the rewound steps records them as skipped even though they ran.
  • [j] jump — jump forward to a later step (e.g. steps 3–5 are irrelevant this run and you want to go straight to step 6). Shows a numbered picker of the steps after the current one (type the listed number, the manual's step number such as 2.7, or a unique part of the step name); the current step and every step up to (but not including) the target are recorded as skipped (⏭) in the run report, and execution resumes at the target. The append-only JSONL audit log keeps the full history. Not offered on the last step. To start a fresh run partway through instead, use run --from-step <N> (see below).

Evidence-required gate ​

When a step declares evidence: { required: true }, completing it (pressing Enter/notes on a manual/sidecar step, or approve on an approval step) is blocked until at least one item has been captured via [e]. There is no separate gate menu — trying to complete with nothing captured just prints a one-line warning on the same action bar, so you never leave it:

    ⚠️  This step requires evidence — press [e] to capture (typed text is fine) or [s] to skip.
  • [e] — runs the same [e] capture flow described above; a captured item (including a quick typed-text note) satisfies the requirement, and the step completes on your next Enter. If you can't attach real evidence right now, type a note explaining why via [e] — it's recorded in the run report as the audit trail.
  • [s] — skip the step. It's recorded as skipped (not completed) in the run record, distinct from a completed step.

approval steps get an [e] evidence key on their own bar too, so they're satisfied the same way (capture, then approve).

This is on by default — declaring evidence.required: true is enough, no flag needed. To disable enforcement (e.g. a CI rehearsal or dry-run walk-through), pass --no-require-evidence to run or resume. The gate applies to manual/sidecar step completion and to approval steps' approve path; it does not gate plain (non-sidecar) type: automatic steps. See examples/evidence-required-step.yaml.

Mock run (--mock): replay expect against captured evidence ​

samaritan run <op> --env <env> --mock validates your verification rules without a terminal, tmux, or executing anything. For each step that defines expect, it pulls the command_output/log entries from evidence.results[<env>] (inline content or a referenced file), runs them through the same assertOutputDetailed engine the interactive [v] verify uses, and prints a per-step PASS/FAIL/SKIP report with the highlighted output:

bash
samaritan run examples/mock-run-expect.yaml --env staging --mock
  • Steps with expect but no replayable evidence for the environment are skipped (reported, not failed).
  • ${VAR} references in expect are resolved against the environment's variables, exactly as a real run would.
  • Exits non-zero if any assertion fails — so CI can catch the day your expect rules drift from the output you actually capture.

This reuses evidence.results read-only; it doesn't change what evidence is for. See examples/mock-run-expect.yaml.

The run record (durable, beside the operation) ​

Every samaritan run/resume writes a run record next to the operation file, so the audit trail travels with the operation you ran (operations-as-code) and can be committed or attached to a change ticket:

<operation-dir>/.samaritan-runs/<session-id>/
├── events.jsonl   # append-only "black box": every step start, command, capture,
│                  # verification, evidence, approval, and rollback (paths printed as 📝 Audit log)
└── report.md      # human-readable Markdown report, written automatically every run (📄 Report)
  • Always-on: report.md is generated on every run — --report <dir> now writes an extra copy to a directory of your choice (e.g. an evidence bundle).
  • Local paths redacted: operator-local path prefixes in commands, output, and evidence are stripped so a shared report doesn't leak machine-specific locations — your home directory shows as ~, and the run directory / operation directory are relativized to their tail (e.g. /tmp/operation/runs/JOB-1/email.html → email.html).
  • Append-only across resume: resume continues the same events.jsonl, so the full history survives across processes.
  • Verification & approvals: the report surfaces each expect check (pass/fail with expected-vs-actual) and an Approval Trail (approver, decision, rationale, timestamp).
  • Read-only fallback: if the operation's directory isn't writable, the run record falls back to ~/.samaritan/sessions/<session-id>/.
  • Gitignored by default: .samaritan-runs/ is in .gitignore to keep working trees clean — git add -f a specific run folder when you want to commit it.

Sessions & resume ​

Every samaritan run also creates a resumable session state file at ~/.samaritan/sessions/<session-id>.json, persisted after each completed step (the session ID is printed at the start of the run: 📋 Session: <id>). This file now carries a structured step_log — per step it records the input command(s), captured output, verification result, approval (with rationale), notes, evidence references, status, and timing — derived by folding events.jsonl, so the saved session is meaningful on its own, not just metadata.

bash
# List resumable sessions (running / paused / failed)
samaritan sessions

# Include completed and cancelled sessions
samaritan sessions --all

# Continue a run where you left off
samaritan resume <session-id>

# Or jump to a specific step
samaritan resume <session-id> --from-step 4

# Sidecar: capture from a different pane than the one the run used
samaritan resume <session-id> --attach work:0.1

Sidecar resume re-attaches your pane. The pane a sidecar run captures from (--attach or [t]) is saved on the session as tmux's stable pane id (%3), so resume re-attaches it automatically (🔗 Re-attached capture to tmux pane: %3) and [v] keeps working. The id is used, not a positional target like work:0.0: when a pane closes, tmux renumbers the window, and work:0.0 could then point at a different pane, even samaritan's own. If the saved pane no longer exists, resume warns (Saved tmux pane "%3" no longer exists — press [t] …). --attach overrides the saved pane. Samaritan also refuses to attach to the pane it is running in.

Quitting a run with q, abort, or Ctrl+C stops execution but saves the session as paused, prints the resume command, and keeps it listed in samaritan sessions — so you can stop mid-operation and pick it back up later. (Ctrl+C no longer hard-quits without saving — it now unwinds through the same save path as abort.) Resume restores variables, execution mode, and the current step index, then re-enters the interactive loop.

Named sessions ​

Define where each step runs — local or over SSH:

yaml
sessions:
  execution:
    host: prod-bastion.example.com
    user: deploy
    env:
      KUBECONFIG: /home/deploy/.kube/prod-config
  monitoring:
    host: monitoring.example.com
    user: sre-readonly         # read-only verification host

Variable names. ${VAR} in a session name is resolved against common variables, so sessions can be derived from, say, a ticket id instead of hardcoding. Quote keys that start with ${:

yaml
common_variables:
  ticket: JIRA-1234

sessions:
  "${ticket}":                  # → session "JIRA-1234"
    host: prod-bastion.example.com
    user: deploy
  "${ticket}-local": {}         # → session "JIRA-1234-local" (local pane)

${VAR} in a step's session: reference resolves the same way, so session: ${ticket} lines up with the resolved session key. Remote-vs-local stays config-driven — a config with host: makes Samaritan auto-run ssh; an empty {} is a local pane. See examples/sessions-with-vars.yaml.

Assign a step to a session with session: <name>. Verification can run in a different session than execution:

yaml
steps:
  - name: Deploy
    session: execution
    command: kubectl apply -f deployment.yaml -n ${NAMESPACE}
    verify:
      session: monitoring      # check via the read-only pane
      command: kubectl rollout status deployment/web -n ${NAMESPACE}
      expect:
        contains: "successfully rolled out"

Run modes (auto_send / auto_exec) ​

Control how hands-off the operator experience is:

yaml
run:
  auto_send: false   # true → command auto-loaded into terminal on step start
  auto_exec: false   # true → Enter sent automatically after send
auto_sendauto_execBehaviour
falsefalse[s] loads command, operator presses Enter (most cautious)
truefalsecommand auto-loads, operator reviews then presses Enter
falsetrue[s] loads and immediately executes
truetruefully automatic — no operator intervention

Rule-based assertions ​

Verify command output automatically. Shorthand string = contains:

yaml
verify:
  command: kubectl get pods -n prod | grep web | grep -c Running
  expect: "3"                 # shorthand: output must contain "3"

Or structured:

yaml
verify:
  command: kubectl get pod web-0 -o jsonpath='{.status.phase}'
  expect:
    equals: "Running"
    retry:
      interval: 5s
      max: 12               # retry up to 12 times (60s total)

All supported assertion types:

YAML keyPasses when
contains: "text"output includes substring
not_contains: "Error"output does not include substring
equals: "value"trimmed output exactly equals value
matches: "regex"output matches regular expression
not_empty: trueoutput is non-empty
any_line_contains: "text"at least one line includes substring
no_line_contains: "Error"no line includes substring
all_lines_match: "regex"every non-empty line matches pattern
any_line_matches: "regex"at least one line matches pattern (regex sibling of any_line_contains)
no_line_matches: "Error|FATAL"no line matches pattern (regex sibling of no_line_contains)
line_count: 3exactly N non-empty lines
line_count_gte: 1at least N non-empty lines
numeric_gte: 80first number in output ≥ value
jsonpath: "$.status" equals: "ok"JSONPath expression equals value
equals_captured: VARoutput equals a previously captured variable

Variables in expect: only ${VAR} operation variables (variables:, common_variables:, --var) resolve in expect. A bare shell variable such as contains: $USER is compared literally — the shell that expanded $USER in your command never sees expect, and samaritan does not read its own environment (an SSH pane's $USER would differ). samaritan validate warns (expect-lint:) about every such $NAME, and a failing [v] prints a hint. Use ${USER} with a variable you define, or a literal value.

Checks that pass for the wrong reason: samaritan validate also warns (expect-lint:) when a contains / any_line_contains value also appears in the step's own command (ls -la dist/main.js with contains: dist/main.js) — it can match the echoed command line instead of the output — and when the value is a bare short number (contains: '2' also matches 12 and 20; use matches: '\b2\b' or equals: '2').

Shell prompt: in sidecar capture, the prompt the shell prints after the output (user@host:~$ ) is stripped before checks run, so line_count, all_lines_match, equals and friends see only the command's output. With an attached pane the pane's current prompt line is removed exactly; otherwise an unterminated last line ending in $ # % > ❯ ➜ » λ is dropped.

Regex semantics: matches, all_lines_match, any_line_matches, and no_line_matches compile with Node's new RegExp(pattern) using default flags — case-sensitive, no multiline, and an unanchored partial match (Running matches anywhere; anchor with ^...$ for a full-string/full-line match). samaritan validate runs a built-in regex-lint pass over these fields (and expect.retry.while): an uncompilable pattern is a hard error, and an obviously catastrophic (ReDoS-prone) pattern such as (a+)+ is a warning (promoted to an error under --strict).

Retryable verification (expect.retry) ​

Some checks need a moment to settle (rolling deploys, health endpoints, async jobs). Add a retry block so an automatic step's verify re-captures the pane and re-asserts up to max times, interval apart:

yaml
- name: Wait for rollout
  type: automatic
  command: kubectl rollout status deployment/web -n staging
  expect:
    contains: successfully rolled out
    retry:
      interval: 5s          # '5s', '500ms', '2m', or a bare number (ms)
      max: 10               # stop after 10 retries
      while: timeout|503    # OPTIONAL: only retry while output looks transient
  • Without while, any failure is retried until it passes or max is hit.
  • With while (substring or regex), only failures whose captured output matches the pattern are retried — a non-transient failure (e.g. a permission error) fails fast instead of burning the remaining attempts. This is the "retryable code / retryable message" guard.
  • Polling applies to the automatic-step verify path; manual [v] verify stays operator-driven (press [v] again to re-check).
  • The retry policy is also rendered in generated manuals (Markdown, Confluence, and ADF) as an extra Expected criterion, e.g. retry up to 10× every 5s (or … while "<guard>" when a while guard is set), so the runbook documents the polling behaviour alongside the assertion.

See examples/expect-retry.yaml.

Capture — carry values forward ​

Extract values from command output and use them in later steps:

yaml
steps:
  - name: Build image
    command: docker build -t myapp .
    capture:
      IMAGE_ID:
        pattern: "Successfully built ([a-f0-9]+)"
        group: 1            # capture group 1 from the regex
      LAST_LINE:
        line: last          # or: line: first

  - name: Deploy
    command: kubectl set image deployment/web web=myapp:${IMAGE_ID}
    verify:
      command: kubectl get pod -l app=web -o jsonpath='{.items[0].spec.containers[0].image}'
      expect:
        contains: "${IMAGE_ID}"    # ${VAR} interpolated at runtime

Released under the ISC License.