7.2 KiB
Product Spec: --jq filter flag for oz run get and oz run list
Linear: REMOTE-1393 Figma: none (CLI-only change)
Summary
Add a --jq "<filter>" flag to oz run get and oz run list. When set, the command runs the server's JSON response through the given jq filter (using the jaq Rust implementation) and writes the filter's result to stdout instead of printing the raw response.
Problem
REMOTE-1374 landed --output-format json on both commands and deliberately deferred first-class JQ-style filtering as a follow-up. In practice, the scripting pattern it unlocks still requires an external jq binary on the user's PATH:
oz run list --state failed --output-format json | jq -r '.runs[].task_id'
That's fine for interactive use but awkward in CI, sandboxes, or remote environments where jq is not guaranteed to be installed, and it adds a process boundary that complicates error handling and quoting. Bringing filtering into the CLI itself makes one-off inspection commands easier and keeps scripts portable.
Behavior
-
Both
oz run listandoz run getaccept an optional--jq <FILTER>flag.<FILTER>is a jq filter expression in the dialect implemented byjaqwith the full standard library enabled (viajaq-all). When not set, both commands behave exactly as they do today. -
When
--jqis set, the input to the filter is the same JSON document the command would emit under--output-format json:- For
oz run list, the full list response envelope, e.g.{ "runs": [...], "page_info": { ... } }. - For
oz run get <run_id>, the single run object returned byGET /api/v1/agent/runs/:runId.
- For
-
--jqand--output-formatinteract as follows:--jqimplies JSON mode: whatever--output-formatis set to (including the defaultpretty), the JSON fetch path is used to obtain the response, and the filter's output is what is printed. Thepretty/texttable renderers are skipped entirely.--output-format jsontogether with--jqis accepted and equivalent.--output-format texttogether with--jqis accepted and equivalent (table output is skipped; only the filter output is printed).
-
Output format of the filter's result:
- Each filter output value is printed on its own line, terminated by
\n. - Scalar values are printed as raw text, not JSON-encoded. Specifically: strings are printed without surrounding double quotes and without JSON escapes; numbers are printed in their natural form;
true,false, andnullare printed as the literal wordstrue,false, andnull. This matchesgh --jq's behavior (cli/cli#3012) and meansoz run list --jq '.runs[].task_id'prints one bare task ID per line, not one JSON-quoted string per line. - Non-scalar values (arrays and objects) are pretty-printed as JSON across multiple lines, consistent with the non-filtered
--output-format jsonbehavior. --jq .on either command therefore produces byte-identical output to the same command with--output-format json(the top-level response is always an object; objects go through the JSON path).- If the filter produces zero output values (for example,
empty), the command prints nothing and exits0.
- Each filter output value is printed on its own line, terminated by
-
Filter errors are surfaced clearly:
- A filter that fails to parse (syntax error, unknown function, etc.) causes the command to exit with a non-zero status and prints a human-readable error to stderr that includes the filter text and, where
jaqprovides it, a location within the filter. Parse errors fire before any HTTP request is issued, so a typo in--jqnever triggers a network call or consumes credits. - A runtime error during filter execution (for example,
.foo | .baron a number) causes the command to exit with a non-zero status and prints thejaqruntime error message to stderr. Any output values already produced before the error are still written to stdout.
- A filter that fails to parse (syntax error, unknown function, etc.) causes the command to exit with a non-zero status and prints a human-readable error to stderr that includes the filter text and, where
-
--jqdoes not change which HTTP requests the CLI makes: on success, exactly oneGET /api/v1/agent/runs(or.../runs/:runId) request, identical to the request issued by--output-format jsonwithout--jq. On a filter parse error, zero requests are made (see invariant 5). The filter is applied entirely client-side on the parsed response. -
--jqdoes not change server-side filtering behavior. Existingoz run listfilter flags (--state,--source, etc.) compose with--jq: the server narrows the response first, and the jq filter runs on the narrowed response. -
--jqis documented inoz run list --helpandoz run get --help. The help text:- Names the flag (
--jq <FILTER>). - Explains that it runs the command's JSON response through a jq filter.
- Notes that
--jqimplies JSON output and that the filter dialect is jq-compatible viajaq. - Gives at least one example invocation.
- Names the flag (
-
Invariants that must not regress:
oz run listandoz run getwith no--jqproduce byte-identical output to today for each--output-format.- The
--output-format jsonpayload shape is unchanged — the filter reads the existing response, not a new one. --jq ""(empty string) is rejected as an invalid filter via the same error path as any other parse error; it does not silently pass input through. Users wanting pass-through should omit--jqor use--jq ..- Scalar unwrapping applies only to the top-level filter output. Scalars that appear inside an array or object are JSON-encoded as usual (so
--jq '.runs'quotes string fields liketitleinside the returned array, but--jq '.runs[].title'does not).
Non-goals
- No explicit
-r/--raw-outputflag. Scalar unwrapping (invariant 4) covers the commonjq -ruse case; users who want string values to be JSON-quoted at the top level can wrap the filter, e.g.--jq '. | tojson'. - No
--jq-from-file, variables (--arg), or multi-input streaming. The scope is a single inline filter applied to the single JSON response. - No
--template/ Go-template alternative (gh formatting docs). Separate ask if we ever want it. - No TTY-aware colorization or compact-when-piped output (cli/cli#7236). Output is always pretty-printed for non-scalars, same as
--output-format jsontoday. - No
--jqon other commands (e.g.oz run conversation get,oz environment list). The implementation is structured so that adding--jqto any command that already usesprint_raw_jsonis a small, mechanical follow-up, but those rollouts are out of scope here. - No first-class
jq-style error codes (jq reserves specific exit codes for "no output" / "false/null output"). The CLI uses its normal success/failure exit codes.
Example invocations
# Pull just the task IDs of failed runs from the last day.
oz run list --state failed --updated-after 2026-04-20T00:00:00Z \
--jq '.runs[].task_id'
# Get a single field from a run.
oz run get 01HX... --jq '.state'
# Compose with other flags; --jq runs after server-side filters.
oz run list --source CLI --limit 100 \
--jq '[.runs[] | select(.harness == "claude")] | length'