Terminal output
At a terminal, shep draws a box-drawn table that fits your window, colours a sheep's status, restart count, exit code, CPU and memory, and shows status as a face too. Piped into anything else, it prints the same plain columns it always has. You do not have to configure that; it is the default both ways.
What a terminal shows
shep flock in a 120-column window. Every colour here says something a number alone would take a second glance to notice: STATUS carries a face and the word; RESTARTS and EXIT turn a colour once a sheep has actually restarted or crashed; CPU and MEM ramp with how much of each a sheep is using; ID and FOLD are muted throughout, since they never change; and every - is muted too, so an absent value does not compete with a real one:
CPU and MEM have two tiers each, not a gradient. The question they answer is whether one sheep is unusual for this flock, not how much it is using: 160M and 4G both land in the same tier, and on a flock where everything is large the whole column is one colour and tells you nothing. A third tier would have to be the fault colour, which would paint a healthy 4G service as though it had broken. Use --format json when the exact number is the thing you need.
host cpu ██░░░░░░░░ 15% mem ███████░░░ 35.8G / 48.0G disk r 32.0K/s w 192.5M/s net rx 10.5K/s tx 1.2K/s ┌────┬──────┬───────────────┬───────┬──────────┬──────┬─────┬──────┬──────┬────────┬─────────┬──────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ EXIT │ CFG │ CPU │ MEM │ UPTIME │ FOLD │ SMIT │ ├────┼──────┼───────────────┼───────┼──────────┼──────┼─────┼──────┼──────┼────────┼─────────┼──────┤ │ 1 │ api │ (x.x) errored │ - │ 15 │ 1 │ - │ - │ - │ 0s │ - │ - │ │ 0 │ web │ (o.o) online │ 29678 │ 0 │ - │ - │ 0.6% │ 3.0M │ 1m 9s │ backend │ - │ └────┴──────┴───────────────┴───────┴──────────┴──────┴─────┴──────┴──────┴────────┴─────────┴──────┘
The line above the table is the machine. See the host line.
SMIT is a marker a dog attaches to a sheep over the client protocol — - for every sheep here because none carries one. See terminology.
The faces
Five faces across six statuses, because two of them mean the same thing to someone glancing at a table:
(o.o) grazingonline(o~o) wakingstarting(>_<) flinchingwaiting to restart(-.-) asleepstopping, stopped(x.x) startlederrored(?_?) puzzledsilent, dogs onlysilent is not a lifecycle status and no sheep ever wears it. See the dogs table.
They are ASCII rather than emoji on purpose. An emoji is double-width, inconsistently so across terminals, and cannot take a foreground colour, which would turn every column measurement into guesswork.
The EXIT column
A sheep that is not running shows why it stopped: an exit code, or a signal name. A running one shows -, the same convention PID and CPU use for a value that has no honest answer yet.
It tells two failures apart that status alone cannot. errored with fifteen restarts says only that a sheep is not up. EXIT 1 says it started and crashed; an empty EXIT on that same row says it never started at all.
The CFG column
A sheep with config waiting on it, or with settings of its own, is marked here; every other sheep reads -. !2 means two fields are parked for this sheep's next spawn, left there by a Flockfile load that would have had to kill the process to apply them, and shep reload <name> promotes them.
A count and not the names, because the names are what shep describe lists underneath its table and a three-character cell has room for one number. See Overrides.
The cell has a second marker, *2, for fields an operator has taken ownership of, whether or not the sheep's current Flockfile declares them. The door that writes them is the config pane: e on a sheep in shep lookout, behind --allow-control. Pending takes the cell when a sheep has both: a parked field is one command away from taking effect, while an override has been true since the last load.
Multi-instance apps group
An app with instances above 1 gets a header row above its slots, in full or plain: web ×3 in NAME, and a ↳ :0 row beneath it per instance, which is also what web:0 selects. A single-instance app is unchanged, no group row and no suffix:
┌────┬────────┬──────────────┬───────┬──────────┬──────┬──────┬──────┬────────┬──────┬──────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ EXIT │ CFG │ CPU │ MEM │ UPTIME │ FOLD │ SMIT │ ├────┼────────┼──────────────┼───────┼──────────┼──────┼─────┼──────┼──────┼────────┼──────┼──────┤ │ │ web ×3 │ (o.o) online │ │ 0 │ │ │ 0.0% │ 9.1M │ 33s │ - │ - │ │ 0 │ ↳ :0 │ (o.o) online │ 74802 │ 0 │ - │ - │ 0.0% │ 3.0M │ 33s │ │ │ │ 1 │ ↳ :1 │ (o.o) online │ 74803 │ 0 │ - │ - │ 0.0% │ 3.0M │ 33s │ │ │ │ 2 │ ↳ :2 │ (o.o) online │ 74804 │ 0 │ - │ - │ 0.0% │ 3.0M │ 33s │ │ │ └────┴────────┴──────────────┴───────┴──────────┴──────┴─────┴──────┴──────┴────────┴──────┴──────┘
The group row rolls up rather than picking a winner: STATUS is the shared word when every instance agrees, else a count per state like 2 up, 1 down. RESTARTS, CPU and MEM sum across the instances, and UPTIME is the minimum, time since the last disruption. ID and PID are blank, since neither has a single answer, and FOLD and SMIT sit only on the group row and stay blank on the instance rows beneath it, since both are per-app already. When a selector or filter matches only some of an app's instances, the count and rollups describe the rows actually listed, not the app's true size.
bare and --format json stay one line per instance instead, so they stay greppable. bare suffixes the name, web:2, and only for an app with more than one instance:
ID NAME STATUS PID RESTARTS EXIT CFG CPU MEM UPTIME FOLD SMIT 0 web:0 online 74802 0 - - 0.0% 3.0M 38s - - 1 web:1 online 74803 0 - - 0.0% 3.0M 38s - - 2 web:2 online 74804 0 - - 0.0% 3.0M 38s - -
JSON rows carry the slot as its own field, "instance": 0, rather than folding it into the name.
The dogs table
Dogs get their own table, under a Dogs caption, wherever one appears beside a flock listing. Every column the two tables share sits in the same place, and each table's own columns come last:
ID NAME STATUS PID RESTARTS EXIT CPU MEM UPTIMECFG FOLD SMITSOURCE┌────┬────────┬──────────────┬───────┬──────────┬──────┬─────┬──────┬────────┬────────┬──────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ EXIT │ CFG │ CPU │ MEM │ UPTIME │ FOLD │ SMIT │ ├────┼────────┼──────────────┼───────┼──────────┼──────┼─────┼─────┼──────┼────────┼────────┼──────┤ │ 0 │ golbat │ (o.o) online │ 48182 │ 0 │ - │ - │ - │ 3.0M │ 2s │ backed │ - │ └────┴────────┴──────────────┴───────┴──────────┴──────┴─────┴─────┴──────┴────────┴────────┴──────┘ Dogs ┌────┬────────────┬──────────────┬───────┬──────────┬──────┬─────┬──────┬────────┬─────────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ EXIT │ CPU │ MEM │ UPTIME │ SOURCE │ ├────┼────────────┼──────────────┼───────┼──────────┼──────┼─────┼──────┼────────┼─────────┤ │ 1 │ log-rotate │ (o.o) online │ 48207 │ 0 │ - │ - │ 6.6M │ 2s │ adopted │ └────┴────────────┴──────────────┴───────┴──────────┴──────┴─────┴──────┴────────┴─────────┘
CFG, FOLD and SMIT are missing from the dogs table because they are impossible for a dog rather than merely empty: a dog belongs to no fold, a smit is a mark a dog paints on a sheep, and no Flockfile load ever reaches a dog, so nothing can park config on one. A column that reads - on every row teaches nothing.
Colours match too, column for column. Same face and status colour, same restart colour above zero, same ramps for CPU and MEM, same EXIT. So the same dog reads the same way under shep dogs, under shep flock, and after shep restart log-rotate.
A dog that never answers reads silent
A dog is a peer as well as a process. It dials back to the shepherd and keeps a connection open, and one that never manages it is not doing its job however alive it looks. So online becomes silent, in amber, with a puzzled face instead of a grazing one.
What to do about it does not fit in a cell, so the table points at where it lives. One line under the dogs table names every silent dog, and shep describe <dog> carries the long form.
What that adds is whether the shepherd has given up. A dog silent past five seconds is restarted once from the binary on disk; if the restarted dog stays silent, the shepherd stops and never restarts it again. Nothing on the row changes when that happens — it still reads silent, because the process is still up and still has not answered — so shep describe is the only place that says so.
Both of them send you to shep bleats <dog>, and that is deliberate. The shepherd writes what it actually saw into the dog's own log at the moment it gives up, and the fix follows from that. A version skew is one cause: cargo install replaced the binary on disk and the process still running is the old one, so a reinstall and a shep restart <dog> fixes it. A dog that reaches the shepherd on every connection and never names itself in its handshake is another, and reinstalling the same build does nothing at all for that one.
Only online is replaced. starting already says the connection is not up yet, and a dog is silent for a moment every time it starts. --format json carries both facts as handshook and dog_stale, beside a status that still reads online — true of the process, which is what that field has always described.
SOURCE says whose code is running
It is the only column in shep carrying a trust distinction, so the two values do not look alike. built-in is shep running its own code, and is muted like any other label you read past. adopted is a third-party binary running at the shepherd's own trust level, from a path you supplied, with no sandbox around it. That gets the same colour a restart count above zero gets: worth a glance, not a fault, because you chose it.
enable, disable, adopt and rehome print a one-row confirmation with the same treatment. Their STATUS holds either a status or a sentence saying why no shepherd answered, and only a status is coloured; a sentence is left plain, because a colour repeating the words beside it is decoration rather than information.
Where else colour appears
Every table shep prints follows one rule: a colour carries information, or the column does not get one. Beyond the flock and the dogs:
trigger, signal, whisperOUTCOME by what happened: it worked, nothing to report, a config gap you can close, or it failedflush, startupRESULT the same way. absent is muted, not marked: it is the state the verb was asked to producebarksSINKS, so an alert that failed to deliver does not read like one that arrivedkillSOCKET_REMOVED, because a false is what the next boot has to contend withimportREUSE_PORT when it is true, which is work you have to dodescribe's lamb tree (bar the muted - of an unlabelled lamb), delete's id list, unset's count, the two saved-roll tables, the KV store, and shep dogs --available. Each is either pure data shep has no opinion about, or a single column that is also the whole content, where muting would fade the table and distinguish nothing.The roll listing is the one table with a STATUS column and no colour on it. Everything in a saved roll is stopped by definition, so the cell reads stopped on every row of every rendering, and a colour that never varies distinguishes nothing.
After a lifecycle command
start, stop, restart, reload, delete and stock all print the whole flock afterwards, not only the sheep they touched. The question after starting one app is usually what everything else is doing, and the exit code already said whether the one you named worked.
┌────┬────────┬─────────┬───────┬──────────┬─────────┬─────┬────────┬────────┬────────┬──────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ EXIT │ CFG │ CPU │ MEM │ UPTIME │ FOLD │ SMIT │ ├────┼────────┼─────────┼───────┼──────────┼─────────┼─────┼─────┼────────┼────────┼────────┼──────┤ │ 4 │ golbat │ online │ 14459 │ 5 │ - │ - │ - │ 416.0K │ 0s │ backed │ - │ │ 5 │ koji │ stopped │ - │ 2 │ SIGTERM │ - │ - │ - │ 0s │ backed │ - │ └────┴────────┴─────────┴───────┴──────────┴─────────┴─────┴─────┴────────┴────────┴────────┴──────┘ Dogs ┌────┬────────────┬────────┬───────┬──────────┬──────┬──────┬──────┬────────┬─────────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ EXIT │ CPU │ MEM │ UPTIME │ SOURCE │ ├────┼────────────┼────────┼───────┼──────────┼──────┼──────┼──────┼────────┼─────────┤ │ 6 │ log-rotate │ online │ 14110 │ 1 │ - │ 0.0% │ 6.6M │ 41s │ adopted │ └────┴────────────┴────────┴───────┴──────────┴──────┴──────┴──────┴────────┴─────────┘
That is the same output shep flock gives, dogs table and all, so shep restart log-rotate and shep flock draw the same dog the same way. See the dogs table for why it is a second table rather than another row.
delete is the one verb whose listing cannot show what it did, since the rows are gone. It names the ids it removed on stderr and prints what is left on stdout.
notice[delete]: deleted 1 sheep, id 5
--format json is not widened. A script that runs shep stop web --format json asked about web, so data holds the rows for web. Use shep flock --format json when you want the flock.The host line
The line above the table is the machine the flock runs on: CPU across every core, memory in use against memory installed, and what the disks and the network moved over the last second. Loopback is left out of the network figures, since a sheep answering a local proxy would otherwise count every request twice.
-. Memory is not a rate. - means not measured, never zero.host cpu ░░░░░░░░░░ - mem ███████░░░ 35.9G / 48.0G disk r - w - net rx - tx -
shep lookout draws the memory bar from the same figures. A shepherd older than this feature prints no host line at all, and upgrading the binary is not enough on its own: run shep daemon reload.
Row order
Every listing shep prints is sorted by name, then by id. That covers shep flock, every lifecycle verb above, shep lookout, and the tables signal, whisper and trigger print.
Ids are handed out in registration order, which is a fact about your shell history rather than about the flock, and they move: a delete all followed by a fresh start renumbers everything. Names do not. The id breaks ties, so the four instances of an app stocked to four stay in a fixed order instead of shuffling between refreshes, and it is still what you type at shep stop 11.
Narrow windows
The table measures your terminal and drops columns until it fits, least useful first, and names what it hid. The same flock at 60 columns:
host cpu ██░░░░░░░░ 15% mem ███████░░░ 35.8G / 48.0G … ┌────┬──────┬────────┬───────┬──────────┬──────┬────────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ MEM │ UPTIME │ ├────┼──────┼────────┼───────┼──────────┼──────┼────────┤ │ 1 │ api │ (x.x) │ - │ 15 │ - │ 0s │ │ 0 │ web │ (o.o) │ 68630 │ 0 │ 3.0M │ 18s │ └────┴──────┴────────┴───────┴──────────┴──────┴────────┘ CFG, CPU, EXIT, FOLD, SMIT hidden. Widen the window, or use --format json.
Three things narrowed there. The host line is cut from the right and says so with a …: the two bars are what you scan first, so disk and network go before them. Then the status word went before any whole column did, leaving the face, which is five columns rather than fifteen. Then SMIT, FOLD, CFG, EXIT and CPU dropped, in that order, and the footer says so. SMIT goes first: it is by far the widest column, so dropping it buys back the most room.
--format json always carries every field regardless of how wide your window is. If you are scripting against shep, that is the surface to use.ID, NAME and STATUS never drop. Below the width those three need, the table prints wider than the window rather than losing the columns that say which sheep a row is about.
What a pipe gets
Redirect it and the boxes, colour and faces all go away. This is not a setting; a non-terminal stdout forces it, and so does --format json:
host cpu 15% mem 35.8G / 48.0G disk r 32.0K/s w 192.5M/s net rx 10.5K/s tx 1.2K/s ID NAME STATUS PID RESTARTS EXIT CFG CPU MEM UPTIME FOLD SMIT 1 api errored - 15 1 - - - 0s - - 0 web online 29678 0 - - 1.0% 4.1M 1m 9s backend -
The host line keeps its numbers and loses both bars, on the same dial that takes the rules off the table. It is not cut either: a pipe has no window, and trimming to the 80 columns shep assumes would lose real figures to a guess.
The columns themselves do move between releases, though. CFG, EXIT and SMIT are all recent, so parse --format json instead if a script depends on the shape.
Errors and notices
An error always goes to stderr. A notice goes to whichever stream its command chose: to stdout when the notice is the answer, as shep style does, and to stderr when it rides beside some other output, so that --format json stays pipeable. Both name their code on the first line and indent everything after it:
error[usage]: no flock at /tmp/no-such did you mean to drop --home/$SHEP_HOME? the default is ~/.shep to set up a flock there deliberately: mkdir -p '/tmp/no-such'
--home/%SHEP_HOME%, the default reads %USERPROFILE%\.shep, and the remedy wraps the path in double quotes after a bare mkdir. Neither cmd.exe nor PowerShell takes -p, and cmd.exe reads a single quote as an ordinary character rather than as quoting.Only that first line starts at column 0. So a script anchoring error[ or notice[ to the start of a line gets one match per diagnostic, however many lines it runs to. That is the reason for the indent rather than a side effect of it: a path or a filename shep quotes back at you cannot produce a line that reads as shep's own. --format json puts the same text on one line instead, and is still the thing to parse if you have the choice.
Choosing how much
The three levels above the fold are set with shep style:
$ shep style notice[style]: full (from the default) $ shep style plain notice[style]: wrote plain to /tmp/docdemo/home/shep.toml $ shep style notice[style]: plain (from shep.toml)
With no argument it reports the level in force and where it came from, which is what you want when a shell profile and a config file disagree. Four places can decide, and the last one wins:
fullshep.toml[style] level$SHEP_STYLEper shell--styleper command$ SHEP_STYLE=full shep style notice[style]: full (from $SHEP_STYLE)
NO_COLOR
The NO_COLOR convention is honoured at every level, and it is a separate axis rather than a fourth level. It removes colour and leaves the layout alone, so full under NO_COLOR still draws boxes and still shows faces:
│ 1 │ api │ (x.x) errored │ - │ 15 │ 1 │ - │ - │ 0s │ - │
An empty NO_COLOR= counts as unset, per the convention.
Where sheep appear
Beyond the status column, three moments get art, each one a screen with nothing else on it: a flock with nothing in it yet, a flock that is entirely stopped, and shep muster. The count is capped, so mustering forty processes does not paint a field.
kill, delete or stop. Errors keep their colour, because red is information, and lose everything else. A sheep beside a failure makes it harder to read, and a cheerful one after deleting a service is simply wrong.Following the flock
shep flock --follow keeps the table on screen and paints a fresh one over it every second. --interval changes the cadence. One second is the default and the floor.
host cpu ██░░░░░░░░ 15% mem ███████░░░ 35.8G / 48.0G disk r 32.0K/s w 192.5M/s net rx 10… ┌────┬────────┬────────┬───────┬──────────┬──────┬─────┬──────┬──────┬─────────┬──────┬──────┐ │ ID │ NAME │ STATUS │ PID │ RESTARTS │ EXIT │ CFG │ CPU │ MEM │ UPTIME │ FOLD │ SMIT │ ├────┼────────┼────────┼───────┼──────────┼──────┼─────┼──────┼──────┼─────────┼──────┼──────┤ │ 0 │ web │ online │ 26932 │ 0 │ - │ - │ 0.0% │ 3.1M │ 17m 39s │ - │ - │ │ 1 │ worker │ online │ 27395 │ 0 │ - │ - │ 0.1% │ 3.1M │ 17m 31s │ - │ - │ └────┴────────┴────────┴───────┴──────────┴──────┴─────┴──────┴──────┴─────────┴──────┴──────┘
The host line is the one a one-shot listing prints, fitted to the window again on every redraw. Changing the interval does not change what it measures: the shepherd still reads once a second, so --interval 5 shows one second of traffic every five seconds rather than a five-second average.
A follow needs a live shepherd and a real terminal. A saved roll is one moment, and a follow is about the moments after it. With stdout redirected it refuses outright rather than writing escape sequences into a file, and --format json gets the same refusal.
A flock taller than the window is cut at the bottom, with a line saying how many it dropped. shep lookout scrolls. This does not.
Following logs
shep bleats prints the tail of each log before it starts following, so a sheep that has already died still tells you why rather than showing you an empty screen while the reason sits in a file you had no particular reason to open.
api | FATAL: port in use api | FATAL: port in use
--lines says how much, counted per stream, and defaults to fifteen. --lines 0 follows without replaying anything.
A multi-instance app's prefix carries the slot, so lines from two instances stay tellable apart:
web:0 | listening web:1 | listening web:2 | listening
Shared via merge_logs instead, the backlog read from the one file is printed unattributed, web |, since nothing in the file itself says which instance wrote a line. Lines read live after that come from the daemon's own per-sheep events and carry their slot as normal.