Skip to content

feat: --json field allowlist with --jq / --template filters per command #376

@christso

Description

@christso

Priority: medium · Converges the existing `outputSchema` metadata with runtime enforcement, and gives agents a stable contract.

Problem

`src/cli/json-output.ts` exposes a boolean `--json` flag plus a single envelope:

```ts
interface JsonEnvelope {
success: boolean;
command: string;
data?: unknown;
error?: string;
}
```

There is no way to select which fields appear inside `data`, and no on-the-fly filtering. Consequences for agents:

  • Agents must serialize the entire `data` payload to find one value.
  • Adding a new field to `data` is potentially breaking — consumers may pin to the current shape and choke on additions.
  • The `outputSchema` field already declared on every `AgentCommandMeta` is documentation-only; nothing enforces it at runtime.

`gh`'s pattern in `pkg/cmdutil/json_flags.go::AddJSONFlags(cmd, &exporter, fields)`:

  1. Per-command allowlist of field names is declared in code.
  2. `--json field1,field2` validates against the allowlist; unknown fields error with a sorted suggestion list.
  3. `--jq ` runs the result through jq.
  4. `--template ` formats the result.
  5. Shell completion for `--json ` returns the allowlist.

Current behavior

```bash
$ allagents plugin list --json
{
"success": true,
"command": "plugin list",
"data": {
"plugins": [...], # full shape, all fields, always
"total": 12
}
}

Field selection is not supported:

$ allagents plugin list --json=name
error: found 1 error
--json=name plugin list
^ Unknown arguments

No --jq / --template:

$ allagents plugin list --json --jq '.data.plugins[].name'
error: --jq is not a known option
```

Expected behavior

```bash

(1) Boolean form still works (backward compatible)

$ allagents plugin list --json
{ "success": true, "command": "plugin list", "data": { "plugins": [...], "total": 12 } }

(2) Field allowlist

$ allagents plugin list --json=name,source
{
"success": true,
"command": "plugin list",
"data": { "plugins": [{"name": "...", "source": "..."}, ...] }
}

(3) Unknown field fails fast with sorted suggestion list

$ allagents plugin list --json=nope
Error: Unknown JSON field: "nope"
Available fields:
clients
install
name
skills
source
exit 2

(4) --jq pipes the envelope through jq

$ allagents plugin list --json --jq '.data.plugins | map(.name)'
["foo", "bar", "baz"]

(5) --template uses ES6-style or Handlebars-style template (pick one)

$ allagents plugin list --json --template '{{range .data.plugins}}{{.name}}\n{{end}}'
foo
bar
baz

(6) --agent-help advertises the allowlist

$ allagents --agent-help "plugin list" | jq .json_fields
["clients", "install", "name", "skills", "source"]
```

Verification gate (must pass before closing)

```bash
set -euo pipefail
bun run build
WS=$(mktemp -d)
cd "$WS"
allagents workspace init --client claude
allagents skills add brainstorming --from anthropics/superpowers

(1) Boolean --json still returns the full envelope (backward compat)

allagents --json skills list | jq -e '.command == "skills list" and (.data.skills | type == "array")'

(2) Field selection returns only the requested fields

allagents --json=name,plugin skills list | jq -e '.data.skills[0] | keys == ["name", "plugin"]'

(3) Unknown field exits non-zero with a sorted suggestion list

OUT=$(allagents --json=bogus skills list 2>&1 || true)
echo "$OUT" | grep -q 'Unknown JSON field'
echo "$OUT" | grep -q 'Available fields'

(4) --jq filter

allagents --json --jq '.data.skills | map(.name)' skills list | jq -e 'type == "array"'

(5) --jq without --json is rejected

! allagents --jq '.data' skills list

(6) Allowlist is discoverable via --agent-help

allagents --agent-help "skills list" | jq -e '.json_fields | type == "array" and length >= 1'

cd / && rm -rf "$WS"
```

All six checks must pass.

Implementation notes

  • Add `jsonFields: readonly string[]` to `AgentCommandMeta` in `src/cli/help.ts`. Populate every existing meta in `src/cli/metadata/*.ts` from its existing `outputSchema`.
  • `src/cli/json-output.ts`: rework `extractJsonFlag` to accept `--json` (boolean), `--json=field1,field2` (or `--json field1,field2`), `--jq `, `--template `. Mutually-exclusive rules:
    • `--jq` requires `--json`.
    • `--template` requires `--json`.
    • `--jq` and `--template` mutually exclusive.
  • Field validation happens at the point of envelope construction: each command knows its meta, the meta knows its `jsonFields` allowlist. Reject unknowns with a sorted Available-fields list and exit 2.
  • For `--jq` use a small embedded jq (e.g., `node-jq`, `jq-wasm`). Bun's runtime supports both.
  • For `--template`: pick a template engine. Simplest is Handlebars or Mustache; closest to `gh`'s Go templates would be `gomplate`-style but that's overkill. Document the choice in the meta.
  • Update `src/cli/agent-help.ts::formatForAgent` to emit a `json_fields` key when the meta defines one.
  • Backward compat: every existing `--json` invocation that returns the full envelope must continue to do so when no field list is given.

Refs

  • Reference impl: `cli/cli` `pkg/cmdutil/json_flags.go` (`AddJSONFlags`, `checkJSONFlags`).
  • Companion wiki page: `concepts/allagents-vs-gh-skill.md` § "--json output contract".

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions