Utility Conventions (shared)
Status: Canonical. These rules define how every GodspeedOS utility behaves.
They were first written inside 1_observe.md §3 and hoisted here once a second
utility existed (per the note in that section - pull the abstraction into
existence, do not build a framework speculatively, §26.2).
Each utility has its own numbered doc in this folder (1_observe.md,
2_date.md, …). This file holds only what is common to all of them.
1. The rules
-
Every utility has
help. A bare utility name with no actionable args, or an explicit<util> help, prints usage. With a fresh (non-POSIX) vocabulary, the system must teach its own verbs at the point of use. -
Every subcommand has
help.<util> <subcommand> helpdescribes that subcommand specifically (e.g.observe now help).At every depth, and checked. Below the first word the answer is the block for the word above -
wifi debug trace helpprintswifi debug's block, which namestracewith one line - because a leaf's one line IS its help and a block per leaf would repeat it. Amended 2026-09-29: an audit of every word Tab could reach found 22 first-level words with no help at all (chaos kill-storm helpreadhelpas a service name and refused it with the list) and nothing at depth three. The answers live in one place,sub_helpinservices/shell/src/main.rs- a command with its own per-word help (events,trace,chaos) is reached from there by a delegating arm, so<util> <word> helphas exactly one answer whichever way the words arrive.scripts/subcmd_help_check.pyreadsSUBCMD_FIRSTandsub_helpand fails on any word Tab offers that answers nothing; it runs on every build. -
helpis the word - the only form. No flags, no synonyms. There is exactly one way to ask for help: the wordhelp. No-h, no--help, no hidden aliases. A tolerated-but-undocumented synonym would itself be a hidden, unsaid rule - the silent behaviour the system forbids (§26.4, §26.5).-his simplyunknown:, and that response teaches the real word.help <word>anddocs <word>are not an exception to this, and the distinction is worth stating because it looks like one.<util> helpgives one command's detail and remains the only way to ask for that.help dirdoes something else: it opens the browsable list already scrolled todir, which is whatmanactually means - find this in the manual. It exists because the browser LISTS the commands, so readingdirthere and typinghelp diris an expectation this interface creates; and because the alternative was worse than either reading, since the argument used to be discarded in silence. A word that matches nothing says(no match)rather than sitting at the top looking like a hit. -
Subcommands are words, never single-letter flags.
observe now, notobserve -n. A word means the same thing across every utility; flag letters collide and drift (-n= "now" here, "number" there). This is thels -Sslahwall-of-letters problem GodspeedOS rejects. Typing economy is a shell-ergonomics concern (completion, history - future), not a reason to abbreviate vocabulary. -
Every utility has a version, reported by
<util> version. Per utility, not per subcommand - subcommands evolve with their parent and inherit its version. -
helpoutput carries the version header. Line 1 of any help/usage output is<util> <version>, so the version is always one keystroke away. -
Utilities report raw facts; they do not editorialize. Health verdicts, policy, and recommendations belong to purpose-built utilities (e.g.
ask,60_ask.md), not bolted onto a metrics or info command. -
Vocabulary is not POSIX. GodspeedOS has no fork/exec/errno/signals, so the user-facing words do not borrow POSIX names that would imply that heritage. Concretely: the clock's epoch subcommand is
date epoch("seconds since 1970"), notdate unix; there is notimeclock (in Unixtimemeasures command duration). Pick words that say what the thing is in this system. -
Every utility and subcommand tab-completes. The name completes from the command list; each subcommand keyword completes at its position (
net a->net arp,net ver->net version,ping c->count). Rule 4 makes words the vocabulary; completion is what makes typing words as cheap as flags. A subcommand that does not complete is one the user cannot discover without reading the source. Wire it in the shell's completion tables (SUBCMD_FIRST/ the per-command case) the same commit that adds the subcommand.Declare whether your arguments are file paths (same commit). When a token is not a recognized keyword, Tab falls through to file-path completion - it lists the current directory (
dir /x<tab>,read /doc<tab>). That is correct only for utilities whose arguments are paths. A utility whose arguments are service names, numbers, or fixed keywords - never paths (chaos,kill,spawn,restart,ping,net,drives,observe,date,uptime, ...) must be added toNO_PATH_CMDSin the shell (services/shell/src/main.rs, besidecomplete_tab), so Tab at one of its argument positions does nothing instead of offering unrelated files. (The bug this prevents:chaos max-carnage all-services <tab>, landing on the rounds argument, listed the root directory and offered/.gsh_history, where the history then lived.) The default is path completion; opting out is explicit and per-command - a path-taking utility (dir,read,write,mkdir,find,tree,copy, ...) is simply absent fromNO_PATH_CMDSand keeps its file completion. So when you add a utility: if its args are paths, do nothing; if they are not, add it toNO_PATH_CMDS.versionandhelpcomplete for free - do not list them. Every utility answers<util> versionand<util> help(rules 5-6), socomplete_keywordappends them to every command's first-argument completion automatically. Do not addversion/helpto aSUBCMD_FIRSTentry (they are deduped in). A utility that takes no positional first argument (an info command:about,version,mem,uptime,status, ...) has the universalversion/helpas its only first-arg subcommands - add it toINFO_CMDS(besideNO_PATH_CMDS) so those two complete and Tab never falls through to a filesystem listing. So a new utility is exactly one of: a path command (in neither list), a keyword command with specific subcommands (SUBCMD_FIRST+NO_PATH_CMDS), or an info command (INFO_CMDS). Pick one;version/helpcome along in every case. 10a. A bare letter is a control ONLY where letters are not text. Full-screen surfaces divide cleanly in two, and the apparent inconsistency between them is forced rather than an oversight:- Read-only views -
help,docs,paginate, the console's scrollback - takeqandEscto leave, because nothing there is typing and a letter is free to mean something. - Typing surfaces -
edit- cannot. A bareqmust insert the letterq, so leaving and saving need a modifier:^Qand^S.
NAVIGATION IS THE SAME EVERYWHERE, and that is the part that must not drift: arrows move a line, PgUp/PgDn a page, Home/End the ends.
Name the two keys that work, not the cluster. A status line reading
[arrows]claims all four when only up and down do anything, and left/right are free to mean something else later. It reads[up/down].In WORDS, because the console cannot draw the symbols.
render::cell_for_codepointpasses ASCII through and maps eleven box-drawing characters; everything else becomes a literal?(deliberately - visible, never silently dropped). So\u2191and\u2193would print as??on a framebuffer while rendering correctly over serial, which is worse than either. Words also match rule 4: a word means the same thing everywhere, and symbols are what that rule exists to avoid.editalready matcheshelp,docs,paginateand the scrollback view key for key on all five. Somebody "fixing"editto quit onqwould break the only rule that actually distinguishes them, so the rule is written down rather than left to be inferred from two examples that look contradictory. - Read-only views -
-
Nothing traps the user: every wait says
[q] quit, by 4 s, and honours it. (Rewritten 2026-10-13 with the operator,backlog/fixed/89,audits/2026-10-13-quit-audit.md.) Any command a person waits on that can run past 2 s says[q] quit- aimed at 2 s, and never later than 4 s on the slowest board, because the shell's clock counts whole seconds and a reply may already be on its way - andq/Q/ESC brings the prompt back within 3 s, saying truthfully what happened: stopped, still running underneath, or outcome unknown.[b] backgroundstands beside it where the work can run detached - a separate program, holding no console authority, whose output fits a transcript - and a command with no[b]says nothing about[b]. Socopy <file> <dst>, `deleterecursive`, `drives check` and `drives scrub`, typed at the prompt, START as the job `background` would start and the console attaches to it: their notice is `[q] quit [b] background`, and `b` leaves them running as job N (`55_background.md`). In a script, a pipe, or while a job is already running they run in the shell and offer `[q]` alone. Waits on the person (a confirm, `input`, a full-screen program with its own quit key) are exempt. The shell is the observer. It starts a COMMAND clock at Enter, and every wait the command makes reads it through
gs::call::Watch- ags::fshandle lent.watching(ctx)(the shell'sctx.fs()),gs::call::request_watched,recv_quittable- so the notice is per command, a command made of many fast requests is covered as well as one slow request, andqstops the command at its next request. A script, awaitandselfcheckstop on it too. (tcp,serveandsockuse ags::nethandle with its own notice: quittable, on the request's clock rather than the command's.) A wait the watch cannot see is a defect to fix; the few that cannot trap anyone - under 2 s, one pass of a loop that readsq, after aq, or between commands - say so beside them (// quit-ok:), andscripts/quit_wait_check.pyrefuses any other.osdev test quitmeasures every kind of wait on anfsbuilt to answer slowly (slow-replies) and writes the table tobuild/tests/quit_report.md.The rule as it read before 2026-10-13, kept as history: Anything that blocks or waits is escapable with
q. If a utility can sit waiting - on a peer service, on the network, on a long sweep - thenq/Q/ESC MUST abort it and return to the prompt, and a wait of more than a moment advertises[q] quit. A command that can wedge the shell with no way out is forbidden (§26.7: loud + escapable over silent + stuck). The primitive isServiceContext::request_with_reply_abortable(send once, pollqwhile waiting); never block an interactive command on a barerequest_with_replyto a peer that can be slow. The fs-backed interactive commands (dir/cd/read/find/treeand the file-reading filtersmatch/count/sort) use the delayed-hint variant: ags::fs::Fshandle lent a notice (Fs::with_notice/Fs::noticing), which waits throughgs::call::request_within_notice. Silent on a fast reply, it advertises[q] quitonly once the wait lingers pastgs::call::NOTICE_AFTER_SECS(2 s) - the just-in-time form of the advertisement above, so a snappy op stays quiet.(Note 2026-10-09: of that list, only
dir's text listing lends the notice today.cd,read,find,treeand the file-reading filters go throughgs::fs::Fshandles built with no notice (fs_stat_r,fs_read_at,filter_read, the walks incmd_find/cmd_tree), so they are bounded atgs::call::DEFAULT_SECSbut cannot be ended withq- a code defect against this rule, recorded in each utility's own spec.)(Note 2026-10-12: the advertisement is now AUTOMATIC on the shell's receive waits, at the operator's standing rule - "for every request that takes time, there should automatically be a
[q] to quit". A pipe stage's drain (drain_service_with, whichaskalso goes through) and a file-cap reply wait throughrecv_quittableinservices/shell, which prints[q] quitonce nothing has come forgs::call::NOTICE_AFTER_SECS, once per wait. The SDK gives the timing (ServiceContext::recv_abortable_deadline_noticing, the receive-side twin of the request'srequest_with_reply_qhint) and the shell the words, because a task with no prompt has nothing to print. A new receive wait in the shell gets it by going through that helper. Since 2026-10-13 the request waits that used plainrequest_with_reply_abortable(fs_request,fs_raw-drives,drives flash,reset,label- andns_abortable) go throughask_with_quit_notice, and the re-waits after an overtaken reply (fs_take_tagged,ns_take_tagged) throughrecv_quittable(backlog/84S6). Not yet covered: the fs handles listed in the note above - covered since 2026-10-13,backlog/fixed/89.)*(Note 2026-10-13: and the radio's and audio driver's asks.
wifi_askandaudio_ask- everywifiandaudioverb,wifi hardwareamong them, which asks each radio twice and could sit for 12 s silent - wait throughask_sifted_quittable(the SDK'srequest_with_reply_keyhint_sifted_keeping), andwifi_ask's wait for answers the driver still owes says it too;qthere printsstopped by qrather than "not answering".observe now's wait for its frame anddate syncsay[q] quitas well. Still silent, and why: the waits on a kernelCallrecorderandcopier(8 to 12 s),timeand thens_deadlinepath tonet-stack(each at its caller's deadline), and the 2 s spawn request to the supervisor. A task blocked in aCallcannot read the console, and the noticing form that can takes whatever reply lands next, which is what theCallis there to prevent: giving them[q]needs a reply-matched wait that returns to the caller between slices, which does not exist yet. Fixed later the same day: all four are under the command's watch (ask_watched,hw_ask),backlog/fixed/89.)*
Worked example, and the reason "never block on a bare
request_with_reply" is written as an absolute. The whole networking surface broke this rule on one helper.ns_request- whattcp,sockandserve's listen all go through - used a barerequest_with_reply, which parks the shell INSIDE the syscall where it cannot poll the console. On a Dell Wyse atcpthat net-stack never answered froze the prompt: no echo, noq, and the machine had to be power-cycled. A deadline was not the fix and adding one first was not enough - it made the freeze 20 seconds instead of forever, and 20 seconds of a dead prompt is still a dead prompt. Only pollingqwhile waiting returns the machine to its operator. Theeventschannel (trace_ask) had the same shape and was worse for it, sinceevents failuresis the instrument you reach for WHEN something is wedged; an instrument that can hang on the thing it is measuring takes the prompt with it.(Superseded 2026-10-13,
backlog/fixed/89: these requests now go throughask_sifted_quittable, say[q] quitpast 2 s and stop the wait onq. Kept as the record of why they once could not.) Recorded exception: thewifiradio power requests.wifi radio on,wifi radio off,wifi radio off hardand the power request at the start ofwifi radio powercycleblock in a kernelCallwith noq, because a shell blocked in one cannot read the console. Each is BOUNDED at 15 s (MAX_SECSinwifi_radioandwifi_radio_hard_off,OP_MAX_MSinwifi_radio_powercycle), and the usual wait is two to three seconds. The long waits that follow- the watch after a restart of the driver - keep
q.56_wifi.mdsection 2 records this per verb.
-
Quitting stops the TASK, not just the shell. When a utility is escaped (rule 10), the escape must abort the actual WORK the utility set in motion - not merely stop the shell from waiting on it. If the utility handed a long job to a peer service and the escape only stops the shell listening, the peer keeps grinding and the very next command blocks behind it: the shell looks free but is not. So a command that drives a multi-step peer operation (a sweep, a batch) drives it step-by-step from the shell, so
qends the loop and the peer is only ever mid-ONE step. Worked example:net scanbegan as one batchnet-stackop (sweep all 254 hosts); pressingqunblocked the shell, but net-stack kept sweeping, so a secondnet scanhung waiting for it. The fix made the shell drive the sweep one host at a time (op 7 -> per-host op 6). Escaping the wait is not enough; escape the work. -
Output is a pipeable structure. A utility's result is data, not decoration: a producer emits either a typed record
Table(docs/records.md, so| where/| select/| to jsoncompose) or plain labelled lines (so| match/| countcompose). Piping is the composition model; output that cannot flow onward is a dead end.Which utilities start a pipe, and why the rest do not (counted 2026-10-02 against the shell's
UTILSand its three producer lists,is_producer_builtin,is_record_producerandis_pipe_producer_service). Of the shell's utilities (itsUTILSlist), 28 start a pipe (two,wifiandchurn, for some verbs only): the record sources (status,dir,find,caps,drives,observe now,uptime,events,trace,jobs, andwifi list), the text sources (echo,read,tree,input,about,version,whatis,mem,cores,date,net,ping,sock,help,pwd(added 2026-10-13),wifi's other reports,churn verify), and therosterservice; plusrandomandtcp(made sources that day) and thegreetexample, which are not inUTILS. Seventeen more are pipe STAGES or SINKS rather than sources -match,count,sort,first,last,where,select,to,from,sum,min,max,avg,paginate,write,assert,result- andresult/assertrefuse to start a pipe with a sentence that names the idiom they belong to. The other 22, each for a reason rather than an omission (the library scripts,serveandgpioare not inUTILSand are listed with them):Not a pipe source Why run,selfcheckOrchestrators: they run their own sub-pipelines, so capturing one nests a pipe inside a pipe on a 256 KiB stack - a hardware-proven crash. They write their own output with save <path>insteadhealth,online,size,busiest(the library)Scripts, run by the interpreter: the same class as run, refused inside another script for the same stack reason. Their contents pipe directly -busiestISstatus | sort reverse memdocs,scrollback,edit, bareobserveFull-screen: a pipe cannot carry a screen you scroll. scrollback saveandobserve noware the capturable formsclear,wait,repeat,cd,home,mkdir,copy,move,rename,delete,seal,spawn,kill,restart,reboot,background,foregroundActions: the value is the effect, and what they print is a confirmation, not data wifi scan,join,leave,forget,radio ...Actions; joinalso reads a passphrase from the console, and piped its prompt would vanish. Refused with a sentence naming the reportschurn <seconds>,churn tear,churn resetActions; churn verifyis the report and pipeschaos,fcapExercisers whose value is what they prove while running; chaoswrites its report withsaveserve,gpioserveruns until stopped;gpiodrives pins - the one read it offers is a single line on the Pi 2 only(Note 2026-10-09: the count above is dated.
audioandhardwarehave since joinedUTILSand both are inis_producer_builtin, soUTILSnow holds 68 names and 29 of them start a pipe; the 17 stages and the 22 non-sources are unchanged.)(Note 2026-10-25: recounted.
UTILSholds 82 names and 34 of them start a pipe -ask,godspeed(its reports only),audio(its reports only) andhardwarejoined, andrandomandtcpare inUTILSnow, as areserve,gpio,spawncapandspawnwired. The 17 stages are unchanged; the other 31 are not sources,shutdownanddownloadamong the actions.) -
If it does not fit the common pipes,
writestill captures it. Any producer's output snapshots to a file with| write <path>(redirection is| write; there is no>, see19_write.md). So even a utility that is not a record source is never trapped on screen - its bytes always have somewhere to go. -
Multiple same-type targets are a COMMA-separated list, never spaced.
kill ehci,xhci,fs,spawn ping,pong,restart fs,events,delete /a,/b,mkdir docs,tmp,fmt a.gsh,b.gsh,chaos max-carnage nic-driver,net-stack- one argument, comma-delimited. NOT spaced (kill ehci xhci fs): the shell tokenizes a line to a small fixed arg count (MAX_ARGS = 8), so a spaced list silently caps at ~3 targets, while a comma-list is a SINGLE token and is not capped by it (a list takes at most 16 items,LIST_MAX; a longer one is refused before anything runs). Comma is also the one uniform rule - the same separator on every command - so the user never has to guess which verb wants which shape. Each target runs the command's normal single-target path (same guards, same per-target report); a failure on one does not abort the rest; the set is bounded. A single target with no comma behaves exactly as before. The keywordall-services(kill only) is the whole system set expressed as one target.
Help output shape (normative)
Every usage row carries both a placeholder signature and a real example - the
placeholder teaches the grammar, the example teaches what real input looks like (not
everyone knows what <path> should be filled with):
<util> <version> - <one-line description>
usage:
<util> <default behaviour>
e.g. <real example>
<util> <subcmd> <what the subcommand does>
e.g. <real example>
<util> version
<util> help
<util> <subcmd> help prints the same shape for that subcommand, without the
version / help footer (help_block in services/shell/src/main.rs).
version output is the name + version number, then the creator credit:
<util> <version>
Copyright (C) 2026 Bankole Ogundero and the GodspeedOS contributors.
2. Built-in vs standalone-service utilities
Two implementation shapes exist, and each utility's doc states which it is:
-
Shell built-in. Runs inside the shell's own protection domain, using caps the shell already holds. Cheap and simple; appropriate for trivial output commands (
echo,clear) and read-only info that the shell is already authorised for (mem,cores,about,date,status,caps). The shell's authority is not widened for these - they only read or print. -
Standalone service brokered by the shell. A separate task the shell spawns, holding a contract-scoped least-authority cap set and nothing more. Used when the utility must not run alongside dangerous authority.
observeis the canonical example: it needs only to read metrics, so it is its own service holding an introspection cap - it cannot kill or restart anything by construction, not by being careful (§3.1, §26.9). See1_observe.md§7.
The dividing question is least authority: if a command would otherwise execute in
the same domain as spawn/kill/restart, prefer a standalone service.
2a. Adding a utility: the eight sites, and where its spec lives
Written down because it was learned by failing. Implementing one verb (wifi, 2026-09-27) needed eight
registrations in services/shell/src/main.rs, and the two that were missed were reported by two
different checkers rather than by any list - which works, but late. The knowledge lived only in the
checkers that enforce it.
| Site | What it gives you | What fails if you miss it |
|---|---|---|
SUBCMD_FIRST | tab completion of the first argument | rule 9; Tab falls through to a directory listing |
SUBCMD_SECOND | completion one level deeper, where the surface has one | rule 9, silently |
NO_PATH_CMDS | Tab offers keywords, not paths, where no argument is a path | a path menu for a keyword position |
UTILS | the <util> help / <util> version intercept | rules 1 and 5 |
| the command dispatch | the verb runs at all | nothing runs |
| the producer dispatch | <util> | write <path> captures it | rules 12 and 13 |
a help_block arm | <util> help prints something | caught by util_help_coverage_problems |
a Row in the help listing | anybody can DISCOVER the verb | caught by facts_check - "an omission ships the feature to nobody who was not watching it being built" |
And where the spec goes, which is not a preference. A spec under utilities/ asserts that the
shell answers that verb, and X-user-vocabulary (Commandment X) enforces it in both directions:
- A spec here for a verb the shell does not answer fails. Implement it, delete it, or document the
ABSENCE (
14_poweroff.mdis the worked example, and the head must say "not provided" or "removed"). - A verb the shell DOES answer with no spec here also fails - a verb discoverable only by reading the source is complexity pushed onto the user (CLAUDE.md 26.11).
So a surface designed before its verb exists belongs in docs/ and moves here the day the shell answers
it. docs/tcp-design.md against 48_tcp.md is that split; docs/wifi.md against 56_wifi.md is the
same, and the file moved three times before anyone wrote this paragraph.
Do not reach for utility_vocab_debt to make either direction quiet. A baseline entry instead of a
fix is what CONTRIBUTING.md names as weakening a gate.
3. Conformance status (as-built, honest)
As of 2026-06-14, every utility conforms. Each one implements its own
<util> help (usage with a real example per row) and <util> version (number +
"Copyright (C) 2026 Bankole Ogundero and the GodspeedOS contributors."), and each real subcommand (date epoch,
observe now, drives flash / label / reset) has its own <util> <subcmd> help.
The shell built-ins are driven by a single help_block helper (the format lives in
one place, so all of them render identically), with <util> help / <util> version
intercepted uniformly in the command dispatch; observe (a standalone service) was
already spec-first conformant. The earlier gap - built-ins documented only by the
top-level help list - is closed.
The last-open item is now closed too: the top-level help command conforms.
Bare help is still the categorised command list, but its first line now carries the
version header (rule 6: help 0.4.0 - GodspeedOS shell commands), and help help /
help version resolve like any other utility's. So every command the shell
dispatches - including help itself - self-documents and reports a version.