Imported from tidepool-heavy-industries/tidepool (
exomonad/examples/workspace/.exomonad/skills/exomonad-command/SKILL.md). Install upstream withnpx skills add tidepool-heavy-industries/tidepool --skill exomonad-command. Copyright stays with the author.
Use hosted bash for direct repository commands. Its structured cmd field
contains literal Bash, including multiline scripts and heredocs; optional fields
select workdir, environment, memory, PTY, and stdin. Haskell Cmd composes the same
command owner when results feed a program.
Call bash with:
{"cmd":"git status --short"}
For a build, test run, log or diff that may be large or failing, say what you
are looking for with focus; the result keeps the relevant sections and names
what it omitted, so there is no need to rerun with sed ranges:
{"cmd":"cargo test -p my_crate --lib","memory_mib":4096,"focus":"the failing test, its assertion and panic message"}
For an expected long check, prefer the project's compiled Haskell focused-check composition: start once, route completion and retain source/count evidence. For a direct command, give an explicit memory limit and let it manage completion:
{"cmd":"cargo test -p my_crate --lib","memory_mib":4096}
Use the returned session_id verbatim. write_stdin with omitted chars polls;
with chars it sends input. For piped stdin, close_stdin: true sends any final
chars before EOF. PTYs reject this flag; use explicit terminal input there.
Known rejection explicitly reports that neither chars nor EOF was submitted; correct
the request. If a write is acknowledged but EOF fails, retry close-only, without chars.
Acknowledgment means the backend accepted the write, not that the child consumed it.
An uncertain write must not be replayed automatically.
cancel_command requests cancellation of the same job, regardless of stdin mode;
its receipt distinguishes the request from terminal outcome and cleanup. Repeated
close/cancel is safe; cancellation preserves an already-finished outcome.
read_output with that ID and stream: "Stderr"
reads diagnostics from the beginning; continue at the returned next_offset.
An inherited job can be inspected with Cmd.status, Cmd.await, Cmd.output,
and positioned reads without moving the owner's display cursor. Input, EOF,
resize, and cancellation remain with the owner. A fresh command constructed
from an inherited helper runs in the calling actor's checkout; an explicit
directory stays fixed.
Recovery reads are contiguous, with an 8 KiB default display budget;
max_output_bytes clamps into 1024..32768 bytes including metadata; any positive
value is accepted. They never use a
head/tail preview. Positions are original bytes, even for lossy UTF-8.
None of these operations reruns the command. A finished nonzero exit is a command
result; inspect its diagnostics. Terminal receipts always show cleanup separately.
Running or queued means the same job remains
owned. Do useful independent work or route completion rather than repeatedly
polling through model turns. Cmd.completion is an actor EventSource, not an Await
value for watch; see the routing example linked below.
The updated workspace package includes this compiled staged background example
at .exomonad/workspace/checks/background-command-example.hs (template source:
exomonad/examples/workspace/.exomonad/checks/background-command-example.hs):
start one command, attach a record actor, read a compact projection or complete
evidence later, and finish the observer. Its owning tests also cover nonzero
exit and unavailable capture. An unavailable read remains an explicit issue.
The completion event does not itself wake the model; only an installed
completion route does. Do other useful work while waiting, and return to the
retained job when a wake or later task turn makes that useful. Do not repeatedly
poll for completion.
Starting with no output is ordinary progress. Readable-but-empty output has byte positions; an unavailable-output error is different and keeps the same job.
Direct execution defaults: 1024 MiB and up to 60 seconds awaiting completion.
If still running, the same job receives one completion notice (or its existing
watch owns that wake). Continue useful work; no polling is needed. Explicit
yield_time_ms (0..300000) requests a deliberate snapshot without automatic
notification. background: true returns immediately with completion delivery.
In Haskell, Cmd.observeCompletion and Cmd.observeWithCompletion provide the
same bounded waiting and notification behavior for an existing job. Use completion
events for authored continuations; waiting alone does not collect test evidence.
max_output_bytes is a byte budget, not a token count. Direct execution responses
use at most 32 KiB (default 32 KiB). Output that fits max_output_bytes is shown
whole. Without focus, output over budget is shown as a head and a tail with a
marker naming the omitted byte range per stream, plus a recovery pointer; no
Jev call and no sectioning happen on this path. focus filters the output to
the sections relevant to that text (example: "the failing test and its
assertion"): the output is split into sections, each scored by Jev for
relevance to the focus and recent conversation, and the highest-relevance
sections are packed to fit max_output_bytes, with an omitted: marker
naming the sections left out. A focused call still shows everything, without
scoring, when it already fits the budget. Shortened
output is recoverable only to the extent the job still retains it; follow the
reported output position or gap. Do not rerun merely to obtain hidden output.
Use Haskell for reusable command values, data-dependent follow-ups, or typed
completion routing. Cmd is Tidepool.Command; bash, withMemory, MiB,
GiB and qualified Text as T are loaded:
result <- Cmd.run [bash|git status --short|]
let changed = T.lines <$> Cmd.stdout result
An unbound command statement shows its observation. A command result bound in a
cell shows a compact job, exit-status and stream-byte summary; the complete
observation remains available through the binding. Displaying or reading it
again does not execute the command.
Inside an effectful block, print value emits bounded Display output in execution
order, including output before a later failure. It uses the existing Console effect;
it is not Prelude's Show-based IO print. State-machine actors log this output without
waking a model. Large values still need projections or explicit pages.
Use Cmd.quiet action when an unbound command's observation is unnecessary, or
when suppressing routine presentation inside a larger effectful computation.
Quiet is scoped to that action and does not hide a stopped computation or its
recovery receipt. Nonzero process exits remain in the retained result.
let changed = T.lines <$> Cmd.stdout result
changed
Cmd.stdout purely extracts complete stdout from exit zero, or an explicit issue.
It never waits, reads more, reruns, or substitutes empty text. Stderr completeness
and cleanup are separate. Cmd.decodeWith (Cmd.asJSON @Value) (Cmd.stdout result)
decodes JSON; ordinary Text supports T.lines, filtering and other composition.
If capture is incomplete, Cmd.readStdout (Cmd.job result) explicitly reads
complete retained stdout, or reports why it is unavailable. Repeated await
is not a way to enlarge capture.
Cmd.run command starts and waits up to 30 seconds; Cmd.await job observes an
existing job for the same command job. Both return completed results, including
nonzero exits. On overrun, the interactive workbench stops the current computation
and installs a real jobN :: Cmd.Job binding, named in its receipt. The command
continues. The enclosing result is not bound and subsequent statements do not
run. Inspect Cmd.status jobN, read Cmd.output jobN, or later Cmd.await jobN.
That later observation does not resume the discarded continuation. Do not rerun
the command to recover output. Earlier committed bindings remain available.
A Haskell actor handler instead fails normally; it has no interactive remediation
binding. Use Cmd.start and completion events there for unattended long work.
Cmd.observe (Cmd.Observation 250 8192) job instead returns the current status
normally after a bounded wait, displaying available output without stopping the
enclosing Haskell program.
Commands are reusable values. Quotations preserve literal Bash, including
multiline scripts, heredocs and indentation. Haskell does not interpolate shell
variables or backticks. Bash retains ordinary exit/pipeline semantics; choose
set -euo pipefail when appropriate. Pass dynamic values as arguments:
let preview path = Cmd.withArguments [path] [bash|sed -n '1,20p' -- "$1"|]
Cmd.describe (preview "a path; not shell syntax")
Cmd.argv [program,arg1,arg2] bypasses Bash. Cmd.inDirectory and
Cmd.withEnvironment customize intent. An omitted directory means the actor's
own workspace, and relative paths start there. For an actor with an agent
process of its own that workspace is its sandbox; an actor started with
R.start has no process and no sandbox, so its commands run in whatever
worktree for which it holds an owned handle, or in the source checkout when it holds none —
which is why such an actor can write in the worktree it was given
(git reset --hard in its own checkout works) and nowhere else. Constructing
a command does not snapshot inherited environment or location. Cmd.describe
inspects intent without executing. Use pwd in a command when location is evidence.
A git call that would take committed work off a ref — reset --hard to a
commit that lacks HEAD, rebase --onto or --skip that drops commits,
branch -D of a branch's last ref, a force push over published commits — is
held until the command names the ref's actual tip:
withDiscardIntent (DiscardIntent expectedTip target reason) on the command, or
EXOMONAD_DISCARD_EXPECTED_TIP=<tip> in a bash call's environment. The
refusal names the actual tip and the commits that would lose the ref; a clean
worktree does not waive it.
An actor with no process of its own also has no terminal: run its commands
with piped or closed input, never TerminalInput.
Ordinary commands use 1024 MiB. Choose realistic explicit memory for builds/tests,
e.g. job <- Cmd.start (withMemory (GiB 8) [bash|cargo build|]).
start returns immediately; admission queues automatically. Retain the job,
do other work, and observe it later. Memory is a hard limit and admission weight.
traverse Cmd.run commands works sequentially until completion or a foreground
stop. To start all first, retain jobs <- traverse Cmd.start commands, then
collect with traverse Cmd.await jobs. Collection follows input order; nonzero
exit does not cancel siblings. Interrupted observation leaves jobs available.
Cmd.cancel job requests cancellation; status/await reports outcome and cleanup.
Live handles do not promise recovery after host restart.
Completed results capture up to 1 MiB per stream. Automatic display has a shared
64 KiB budget per Haskell tool response; shortening display does not discard captured
data. inspectFull also has a display allowance; use pages or Haskell projections
for larger values. Foreground observations skip fully displayed pages; shortened captures remain
available for explicit navigation. Explicit reads do not consume output. Read without executing again:
page <- Cmd.output (Cmd.job result)
let relevant = filter (T.isInfixOf "error") (T.lines (Cmd.pageText page))
relevant
next <- Cmd.next page
output begins stdout at byte zero; next advances the page.
Cmd.readOutput Cmd.Stderr job and Cmd.tailOutput Cmd.Stderr job explicitly
select stderr or a diagnostic tail. Pages are immutable, non-consuming 64 KiB
windows. Cmd.pageDetails reports byte positions, gaps and fragments. Current
end while running differs from terminal EOF. Valid UTF-8 is preserved across
forward page boundaries; invalid or lost boundary bytes have replacement text
and explicit lossiness. Complete-stdout extraction rejects lossy content.
Retention keeps a 16 MiB prefix plus 256 KiB tail per stream, subject to a 128 MiB owner budget and the latest 32 completed jobs. Completed logs can be evicted; active streams continue draining when retention fills. A reported gap cannot be repaired by expanding display. Choose a workspace log file for larger or longer-lived evidence. Partial text is diagnostic data, not complete JSON.
Cmd.withStdin provides a pipe; Cmd.withTerminal provides a PTY initially sized
to the owning TUI. Retain the job for Cmd.sendInput, Cmd.closeInput and
Cmd.resize. PTYs use terminal EOF input instead of closeInput.
Cmd.completion job :: R.EventSource Cmd.CommandResult supplies one retained
terminal event, including attachment after completion. To continue automatically:
- Capture the original job in the handler; the event contains outcome and cleanup, not the job or captured output.
- Read retained stdout with
Cmd.readStdout job; includeCommandsin the handler's effect row. HandleLeftexplicitly rather than substituting empty evidence.Cmd.stdoutacceptsCmd.RunResult, not the completion payload. Cmd.readStdoutrequires successful completion and answersLeft (Cmd.Unsuccessful outcome)for anything else, so a failed command's diagnostic output is read withCmd.readOutput/Cmd.nextinstead, retaining outcome and cleanup separately.- Interpret the available evidence and execute the prepared follow-up in that handler. Preserve outcome and cleanup separately from a semantic judgment; stdout alone is not a complete diagnostic bundle for commands using stderr.
- Retain the result and finish the collector when its obligations are settled.
Use exomonad-define-actors for handler construction. Captured jobs permit
inspection while available; they do not transfer command control.
For project-authored direct tools, see Defining compiled tools.
