Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions artifacts/requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6547,3 +6547,52 @@ artifacts:
created-by: ai-assisted
model: claude-opus-4-8
timestamp: 2026-06-06T10:30:00Z

- id: REQ-211
type: requirement
title: "`rivet list --format json --full` emits description/tags/fields in bulk so machine-readable queries don't need N× `get` or a raw YAML parse"
status: implemented
description: |
Finding (#506, dogfooding). AGENTS.md/CLAUDE.md direct agents to
`rivet list --format json` for "machine-readable artifact queries", but
that output is summary-only — `id`, `type`, `title`, `status`, `links`.
It omits `description`, `tags`, and `fields`, so the values agents most
often need (e.g. a `priority` field, the acceptance text) are not
reachable in bulk. The full per-artifact view exists via
`rivet get <ID> --format json`, but recovering fields for the whole
store means one subprocess per artifact (917 in this repo) or parsing
the raw YAML — which is exactly the fallback the next-id memory had to
use. `--variant` emits a merged `fields:` view but requires a variant
config and applies overlays, so it is not a clean "all fields" export.

Fix: a `--full` flag on `rivet list`. When set with `--format json`,
each artifact entry also carries `description`, `tags`, and `fields`,
mirroring `get`'s JSON shape. Additive and opt-in: default output is
unchanged, so existing consumers are unaffected
(`list-output.schema.json` is `additionalProperties: true`). The same
change corrects long-standing drift in that schema: `links` was still
typed as an integer count, but the actual output has been an array of
`{type,target}` objects since #358 — the schema now matches.

Acceptance:
- `rivet list --format json --full` emits, for each artifact,
`description` (string), `tags` (array), and `fields` (object) in
addition to the summary keys; plain `rivet list --format json`
still emits only the summary keys (no `description`/`tags`/`fields`).
- The `--full` `fields` object for a given id equals the `fields`
object from `rivet get <id> --format json`.
- `cargo test -p rivet-cli --test cli_commands` passes (full suite),
including a new test asserting the presence/absence of the rich keys.
- `cargo fmt --check` + `cargo clippy --all-targets -- -D warnings` clean.
- `rivet validate` on the rivet repo still PASS.
tags: [list, json, query, machine-readable, dogfooding, dx]
fields:
priority: should
category: functional
links:
- type: traces-to
target: REQ-007
provenance:
created-by: ai-assisted
model: claude-opus-4-8
timestamp: 2026-06-06T11:00:00Z
38 changes: 38 additions & 0 deletions rivet-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,15 @@ enum Command {
/// by id.
#[arg(long)]
rank_by_backlinks: bool,

/// Include full artifact fields in JSON output — `description`,
/// `tags`, and `fields` — matching `rivet get <ID> --format json`,
/// but in bulk. Without it `--format json` emits only the summary
/// (id/type/title/status/links), so a machine-readable query for a
/// field value (e.g. `priority`) otherwise needs one `get` call per
/// artifact or a raw YAML parse (#506). No effect on text output.
#[arg(long)]
full: bool,
},

/// Show artifact summary statistics
Expand Down Expand Up @@ -2109,6 +2118,7 @@ fn run(cli: Cli) -> Result<bool> {
variant,
orphans,
rank_by_backlinks,
full,
} => cmd_list(
&cli,
r#type.as_deref(),
Expand All @@ -2119,6 +2129,7 @@ fn run(cli: Cli) -> Result<bool> {
variant.as_deref(),
*orphans,
*rank_by_backlinks,
*full,
),
Command::Get { id, format } => cmd_get(&cli, id, format),
// REQ-167 / #426: `trace` is the discoverable namesake verb for the
Expand Down Expand Up @@ -6457,6 +6468,7 @@ fn cmd_list(
variant_arg: Option<&str>,
orphans_only: bool,
rank_by_backlinks: bool,
full: bool,
) -> Result<bool> {
validate_format(format, &["text", "json"])?;
let ctx = ProjectContext::load(cli)?;
Expand Down Expand Up @@ -6543,6 +6555,32 @@ fn cmd_list(
map.insert("inbound_links".into(), serde_json::json!(n));
}
}
// `--full` (#506): include the same rich fields as
// `get <ID> --format json` — description/tags/fields — so a
// bulk machine-readable query can read a field value without
// N× `get` or a raw YAML parse. Inserted BEFORE the variant
// block so a `--variant` merged `fields:` view still wins.
if full {
if let serde_json::Value::Object(ref mut map) = entry {
let fields_json: serde_json::Value = a
.fields
.iter()
.map(|(k, v)| {
(
k.clone(),
serde_json::to_value(v).unwrap_or(serde_json::Value::Null),
)
})
.collect::<serde_json::Map<String, serde_json::Value>>()
.into();
map.insert(
"description".into(),
serde_json::json!(a.description.as_deref().unwrap_or("")),
);
map.insert("tags".into(), serde_json::json!(a.tags));
map.insert("fields".into(), fields_json);
}
}
if let Some(ref name) = variant_name {
// Emit the merged fields view so downstream tooling
// can see the variant-resolved values without a
Expand Down
68 changes: 68 additions & 0 deletions rivet-cli/tests/cli_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,74 @@ fn list_json_artifact_fields() {
}
}

/// REQ-211 / #506: `rivet list --format json` is summary-only (no
/// description/tags/fields); `--full` adds them in bulk, matching the
/// `get <ID> --format json` shape. Verify the presence/absence split and
/// that `--full`'s `fields` equals `get`'s `fields` for the same id.
#[test]
fn list_json_full_includes_rich_fields() {
let root = project_root();
let root_str = root.to_str().unwrap();
let run = |args: &[&str]| {
let mut full_args = vec!["--project", root_str];
full_args.extend_from_slice(args);
let out = Command::new(rivet_bin())
.args(&full_args)
.output()
.expect("execute rivet");
assert!(
out.status.success(),
"rivet {:?} must exit 0. stderr: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice::<serde_json::Value>(&out.stdout).expect("valid JSON")
};

// Plain list: summary-only — no rich keys.
let plain = run(&["list", "--format", "json"]);
let plain_first = plain["artifacts"]
.as_array()
.and_then(|a| a.first())
.expect("at least one artifact");
assert!(
plain_first.get("description").is_none()
&& plain_first.get("tags").is_none()
&& plain_first.get("fields").is_none(),
"plain `list --format json` must NOT carry description/tags/fields"
);

// --full: every artifact carries description (string), tags (array),
// fields (object).
let full = run(&["list", "--format", "json", "--full"]);
let full_arts = full["artifacts"].as_array().expect("artifacts array");
assert!(!full_arts.is_empty());
for a in full_arts {
assert!(
a.get("description").map(|v| v.is_string()).unwrap_or(false),
"--full artifact {} must have a string 'description'",
a.get("id").and_then(|v| v.as_str()).unwrap_or("?")
);
assert!(
a.get("tags").map(|v| v.is_array()).unwrap_or(false),
"--full artifact must have an array 'tags'"
);
assert!(
a.get("fields").map(|v| v.is_object()).unwrap_or(false),
"--full artifact must have an object 'fields'"
);
}

// --full's `fields` for a known id must equal `get`'s `fields`.
let id = full_arts[0]["id"].as_str().expect("id");
let got = run(&["get", id, "--format", "json"]);
assert_eq!(
full_arts[0].get("fields"),
got.get("fields"),
"list --full fields must equal get fields for {id}"
);
}

// ── rivet init then validate roundtrip ──────────────────────────────────

/// Initialize a project, then validate it — the sample artifacts should pass.
Expand Down
27 changes: 26 additions & 1 deletion schemas/json/list-output.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,34 @@
"description": "Status string, or '-' if unset."
},
"links": {
"type": "array",
"description": "Outgoing links (issue #358): each is an object with the link `type` and `target`. Count is recoverable as `.links | length`.",
"items": {
"type": "object",
"required": ["type", "target"],
"properties": {
"type": { "type": "string" },
"target": { "type": "string" }
}
}
},
"inbound_links": {
"type": "integer",
"minimum": 0,
"description": "Number of outgoing links from this artifact."
"description": "Inbound-link count; present only with --rank-by-backlinks."
},
"description": {
"type": "string",
"description": "Full description; present only with --full (#506)."
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Tags; present only with --full (#506)."
},
"fields": {
"type": "object",
"description": "Custom field map; present with --full (#506) or --variant (merged view)."
}
}
}
Expand Down
Loading