Files
galaxy/specs/REMOTE-1393/PRODUCT.md
T

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

  1. Both oz run list and oz run get accept an optional --jq <FILTER> flag. <FILTER> is a jq filter expression in the dialect implemented by jaq with the full standard library enabled (via jaq-all). When not set, both commands behave exactly as they do today.

  2. When --jq is 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 by GET /api/v1/agent/runs/:runId.
  3. --jq and --output-format interact as follows:

    • --jq implies JSON mode: whatever --output-format is set to (including the default pretty), the JSON fetch path is used to obtain the response, and the filter's output is what is printed. The pretty/text table renderers are skipped entirely.
    • --output-format json together with --jq is accepted and equivalent.
    • --output-format text together with --jq is accepted and equivalent (table output is skipped; only the filter output is printed).
  4. 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, and null are printed as the literal words true, false, and null. This matches gh --jq's behavior (cli/cli#3012) and means oz 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 json behavior.
    • --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 exits 0.
  5. 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 jaq provides it, a location within the filter. Parse errors fire before any HTTP request is issued, so a typo in --jq never triggers a network call or consumes credits.
    • A runtime error during filter execution (for example, .foo | .bar on a number) causes the command to exit with a non-zero status and prints the jaq runtime error message to stderr. Any output values already produced before the error are still written to stdout.
  6. --jq does not change which HTTP requests the CLI makes: on success, exactly one GET /api/v1/agent/runs (or .../runs/:runId) request, identical to the request issued by --output-format json without --jq. On a filter parse error, zero requests are made (see invariant 5). The filter is applied entirely client-side on the parsed response.

  7. --jq does not change server-side filtering behavior. Existing oz run list filter flags (--state, --source, etc.) compose with --jq: the server narrows the response first, and the jq filter runs on the narrowed response.

  8. --jq is documented in oz run list --help and oz 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 --jq implies JSON output and that the filter dialect is jq-compatible via jaq.
    • Gives at least one example invocation.
  9. Invariants that must not regress:

    • oz run list and oz run get with no --jq produce byte-identical output to today for each --output-format.
    • The --output-format json payload 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 --jq or 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 like title inside the returned array, but --jq '.runs[].title' does not).

Non-goals

  • No explicit -r/--raw-output flag. Scalar unwrapping (invariant 4) covers the common jq -r use 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 json today.
  • No --jq on other commands (e.g. oz run conversation get, oz environment list). The implementation is structured so that adding --jq to any command that already uses print_raw_json is 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'